-
Notifications
You must be signed in to change notification settings - Fork 293
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
internal/core: calculate elementary cycles
Within each Strongly Connected Component, we want to calculate the cycles. Each node within a component (of two or more nodes) can be involved in one or more cycles. Signed-off-by: Matthew Sackman <[email protected]> Change-Id: I4d1d4e527ab4abe49ac2dabc1d807488fe88c554 Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1202378 TryBot-Result: CUEcueckoo <[email protected]> Unity-Result: CUE porcuepine <[email protected]> Reviewed-by: Marcel van Lohuizen <[email protected]>
- Loading branch information
1 parent
5855903
commit 9ddb0ec
Showing
4 changed files
with
289 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
// Copyright 2024 CUE Authors | ||
// | ||
// 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. | ||
|
||
package toposort | ||
|
||
import "slices" | ||
|
||
type ecNodeState struct { | ||
visitedIncoming []*ecNodeState | ||
blocked bool | ||
} | ||
|
||
func (ecNode *ecNodeState) excluded() bool { | ||
return ecNode == nil | ||
} | ||
|
||
type ecFinderState struct { | ||
cycles []*Cycle | ||
stack []*Node | ||
} | ||
|
||
type Cycle struct { | ||
Nodes Nodes | ||
} | ||
|
||
func (cycle *Cycle) RotateToStartAt(start *Node) { | ||
nodes := cycle.Nodes | ||
if start == nodes[0] { | ||
return | ||
} | ||
for i, node := range nodes { | ||
if start == node { | ||
prefix := slices.Clone(nodes[:i]) | ||
copy(nodes, nodes[i:]) | ||
copy(nodes[len(nodes)-i:], prefix) | ||
break | ||
} | ||
} | ||
} | ||
|
||
// Calculate the Elementary Cycles (EC) within the current Strongly | ||
// Connected Component (SCC). | ||
// | ||
// If the component contains no cycles (by definition, this means the | ||
// component contains only a single node), then the slice returned | ||
// will be empty. | ||
// | ||
// In general: | ||
// | ||
// 1. If a component contains two or more nodes then it contains at | ||
// least one cycle. | ||
// 2. A single node can be involved in many cycles. | ||
// 3. This method finds all cycles within a component, but does not | ||
// include cycles that are merely rotations of each | ||
// other. I.e. every cycle is unique, ignoring rotations. | ||
// 4. The cycles returned are unsorted: each cycle is itself in no | ||
// particular rotation, and the complete slice of cycles is | ||
// similarly unsorted. | ||
// | ||
// The complexity of this algorithm is O((n+e)*c) where | ||
// - n: number of nodes in the SCC | ||
// - e: number of edges between the nodes in the SCC | ||
// - c: number of cycles discovered | ||
// | ||
// Donald B Johnson: Finding All the Elementary Circuits of a Directed | ||
// Graph. SIAM Journal on Computing. Volumne 4, Nr. 1 (1975), | ||
// pp. 77-84. | ||
func (scc *StronglyConnectedComponent) ElementaryCycles() []*Cycle { | ||
nodes := scc.Nodes | ||
nodeStates := make([]ecNodeState, len(nodes)) | ||
for i, node := range nodes { | ||
node.ecNodeState = &nodeStates[i] | ||
} | ||
|
||
ec := &ecFinderState{} | ||
for i, node := range nodes { | ||
ec.findCycles(node, node) | ||
ec.unblockAll(nodes[i+1:]) | ||
node.ecNodeState = nil | ||
} | ||
|
||
return ec.cycles | ||
} | ||
|
||
func (ec *ecFinderState) findCycles(origin, cur *Node) bool { | ||
stackLen := len(ec.stack) | ||
ec.stack = append(ec.stack, cur) | ||
|
||
curEc := cur.ecNodeState | ||
curEc.blocked = true | ||
|
||
cycleFound := false | ||
for _, next := range cur.Outgoing { | ||
if next.ecNodeState.excluded() { | ||
continue | ||
} | ||
if next == origin { // found cycle | ||
ec.cycles = append(ec.cycles, &Cycle{Nodes: slices.Clone(ec.stack)}) | ||
cycleFound = true | ||
} else if !next.ecNodeState.blocked { | ||
if ec.findCycles(origin, next) { | ||
cycleFound = true | ||
} | ||
} | ||
} | ||
|
||
if cycleFound { | ||
ec.unblock(curEc) | ||
} else { | ||
for _, next := range cur.Outgoing { | ||
if next.ecNodeState.excluded() { | ||
continue | ||
} | ||
nextEc := next.ecNodeState | ||
nextEc.visitedIncoming = append(nextEc.visitedIncoming, curEc) | ||
} | ||
} | ||
|
||
if len(ec.stack) != stackLen+1 { | ||
panic("stack is unexpected height!") | ||
} | ||
ec.stack = ec.stack[:stackLen] | ||
return cycleFound | ||
} | ||
|
||
func (ec *ecFinderState) unblockAll(nodes Nodes) { | ||
for _, node := range nodes { | ||
nodeEc := node.ecNodeState | ||
nodeEc.blocked = false | ||
nodeEc.visitedIncoming = nodeEc.visitedIncoming[:0] | ||
} | ||
} | ||
|
||
func (ec *ecFinderState) unblock(nodeEc *ecNodeState) { | ||
nodeEc.blocked = false | ||
for _, previousEc := range nodeEc.visitedIncoming { | ||
if previousEc.blocked { | ||
ec.unblock(previousEc) | ||
} | ||
} | ||
nodeEc.visitedIncoming = nodeEc.visitedIncoming[:0] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
// Copyright 2024 CUE Authors | ||
// | ||
// 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. | ||
|
||
package toposort_test | ||
|
||
import ( | ||
"slices" | ||
"testing" | ||
|
||
"cuelang.org/go/internal/core/adt" | ||
"cuelang.org/go/internal/core/runtime" | ||
"cuelang.org/go/internal/core/toposort" | ||
) | ||
|
||
func TestElementaryCycles(t *testing.T) { | ||
type TestCase struct { | ||
name string | ||
inputs [][]string | ||
expected [][]string | ||
} | ||
|
||
a, b, c, d, e, f, g := "a", "b", "c", "d", "e", "f", "g" | ||
|
||
testCases := []TestCase{ | ||
{ | ||
name: "no cycles", | ||
inputs: [][]string{{a, b, c}}, | ||
expected: [][]string{}, | ||
}, | ||
{ | ||
name: "cycle of 2", | ||
inputs: [][]string{{a, b}, {b, a}}, | ||
expected: [][]string{{a, b}}, | ||
}, | ||
{ | ||
name: "cycle of 3", | ||
inputs: [][]string{{a, b}, {b, c}, {c, a}}, | ||
expected: [][]string{{a, b, c}}, | ||
}, | ||
{ | ||
name: "cycle of 3 and 4", | ||
inputs: [][]string{{a, b, c, d}, {c, a}, {d, b}}, | ||
expected: [][]string{{a, b, c}, {b, c, d}}, | ||
}, | ||
{ | ||
name: "unlinked cycles", | ||
inputs: [][]string{{a, b, c, d}, {d, b}, {e, f, g}, {g, e}}, | ||
expected: [][]string{{b, c, d}, {e, f, g}}, | ||
}, | ||
{ | ||
name: "fully connected 4", | ||
inputs: [][]string{ | ||
{a, b, c, d}, {d, c, b, a}, {b, d, a, c}, {c, a, d, b}, | ||
}, | ||
expected: [][]string{ | ||
{a, b}, {a, b, c}, {a, b, c, d}, {a, b, d}, {a, b, d, c}, | ||
{a, c}, {a, c, b}, {a, c, b, d}, {a, c, d}, {a, c, d, b}, | ||
{a, d}, {a, d, b}, {a, d, b, c}, {a, d, c}, {a, d, c, b}, | ||
{b, c}, {b, c, d}, | ||
{b, d}, {b, d, c}, | ||
{c, d}, | ||
}, | ||
}, | ||
{ | ||
name: "nested cycles", | ||
inputs: [][]string{{b, c}, {e, c, b, d}, {d, f, a, e}, {a, f}}, | ||
expected: [][]string{{a, e, c, b, d, f}, {a, f}, {b, c}}, | ||
}, | ||
} | ||
|
||
index := runtime.New() | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
testAllPermutations(t, index, tc.inputs, | ||
func(t *testing.T, perm [][]adt.Feature, graph *toposort.Graph) { | ||
var cyclesNames [][]string | ||
for _, scc := range graph.StronglyConnectedComponents() { | ||
for _, cycle := range scc.ElementaryCycles() { | ||
fs := cycle.Nodes.Features() | ||
names := make([]string, len(fs)) | ||
for j, f := range fs { | ||
names[j] = f.StringValue(index) | ||
} | ||
cyclesNames = append(cyclesNames, names) | ||
} | ||
} | ||
for _, cycle := range cyclesNames { | ||
rotateToStartAt(cycle, slices.Min(cycle)) | ||
} | ||
slices.SortFunc(cyclesNames, compareStringses) | ||
|
||
if !slices.EqualFunc(cyclesNames, tc.expected, slices.Equal) { | ||
t.Fatalf(` | ||
For permutation: %v | ||
Expected: %v | ||
Got: %v`, | ||
permutationNames(index, perm), | ||
tc.expected, cyclesNames) | ||
} | ||
}) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters