1- import graphlib , { Graph } from 'graphlib'
2- // import { Dependency } from './simulation/types '
3- // import findCyclesAdjacency from 'elementary-circuits-directed-graph '
4- import { Graph as AbstractGraph } from './types '
1+ import graphlib from 'graphlib'
2+ import findCircuits from 'elementary-circuits-directed-graph '
3+ import { Graph } from './types '
4+ import cloneDeep from 'lodash.clonedeep '
55
6- function pruneCore ( graph : Graph ) {
6+ function pruneCore ( graph : graphlib . Graph ) {
77 if ( ! graphlib . alg . isAcyclic ( graph ) ) {
8- // const cycles = graphlib.alg.findCycles(graph);
98 throw new Error ( 'pruning is possible on DAG only' )
109 }
1110 const edges = graph . edges ( )
@@ -22,7 +21,7 @@ function pruneCore(graph: Graph) {
2221 return graph
2322}
2423
25- export const prune = ( input : AbstractGraph ) : AbstractGraph => {
24+ export const prune = ( input : Graph ) : Graph => {
2625 const graph = new graphlib . Graph ( )
2726
2827 Object . values ( input ) . forEach ( node => {
@@ -33,10 +32,10 @@ export const prune = (input: AbstractGraph): AbstractGraph => {
3332 )
3433 } )
3534
36- const output : AbstractGraph = Object . fromEntries (
35+ const output : Graph = Object . fromEntries (
3736 Object . entries ( input ) . map ( ( [ uri , node ] ) => [
3837 uri ,
39- { ...node , dependencies : { } } ,
38+ { ...node , dependencies : { } , dependents : { } } ,
4039 ] ) ,
4140 )
4241
@@ -46,69 +45,111 @@ export const prune = (input: AbstractGraph): AbstractGraph => {
4645
4746 prunedEdges . forEach ( ( { source, target } ) => {
4847 output [ source ] . dependencies [ target ] = output [ target ]
48+ output [ target ] . dependents [ source ] = output [ source ]
4949 } )
5050
5151 return output
5252}
5353
54- /*
55- export function getCycles(dependencies: Dependency[]): Dependency[][] {
56- const nodes = Array.from(
57- new Set([
58- ...dependencies.map(d => d.dependent),
59- ...dependencies.map(d => d.dependency),
60- ]),
61- )
62- const nodeIndexes = Object.fromEntries(
63- Object.entries(nodes).map(([key, value]) => [value, +key]),
64- )
54+ // convert Graph to adjacency list, so we can feed it into the library
55+ // elementary-circuits-directed-graph
56+ const graph2adjacency = ( graph : Graph ) : [ AdjacencyList , string [ ] ] => {
57+ const indexes = Object . keys ( graph ) . sort ( )
6558
66- const adjacency = Array(nodes .length)
59+ const adjacencyList = Array ( indexes . length )
6760 . fill ( null )
6861 . map ( ( ) => [ ] as number [ ] )
69- dependencies.forEach(({ dependent, dependency }) => {
70- const dependentIndex = nodeIndexes[dependent]
71- const dependencyIndex = nodeIndexes[dependency]
72- adjacency[dependentIndex].push(dependencyIndex)
62+
63+ indexes . forEach ( ( id , i ) => {
64+ adjacencyList [ i ] = Object . keys ( graph [ id ] . dependencies ) . map ( id2 =>
65+ indexes . findIndex ( a => a === id2 ) ,
66+ )
7367 } )
7468
69+ return [ adjacencyList , indexes ]
70+ }
71+
72+ type AdjacencyList = number [ ] [ ]
73+ type Cycle = number [ ]
74+ type UriCycle = string [ ]
75+
76+ const findLoops = ( adjacencyList : AdjacencyList ) : Cycle [ ] => {
77+ return (
78+ adjacencyList
79+ // save the index along with value
80+ . map ( ( a , i ) => [ a , i ] as [ number [ ] , number ] )
81+ // filter every adjacencyList item with loop
82+ . filter ( ( [ a , i ] ) => a . includes ( i ) )
83+ // return simple loop
84+ . map ( ( [ , i ] ) => [ i ] )
85+ )
86+ }
87+
88+ export const getCycles = ( graph : Graph ) : UriCycle [ ] => {
89+ // first convert graph into a simple adjacency matrix
90+ const [ adjacencyList , indexes ] = graph2adjacency ( graph )
91+
7592 // try to detect loops (cycles of length 1)
7693 // this is due to the limits of the findCyclesAdjacency, which fails to detect loops
7794 // https://github.com/antoinerg/elementary-circuits-directed-graph/issues/13
7895 // @TODO remove loop detection when the issue is fixed
79- const loops = adjacency.reduce((loops, adj, i) => {
80- if (adj.includes(i)) loops.push([i, i])
81- return loops
82- }, [] as number[][])
96+ const loops = findLoops ( adjacencyList )
97+ // get all the other loops
98+ const otherCycles = findCircuits ( adjacencyList )
99+ // filter out loops, so we avoid duplicates (see the above-linked issue)
100+ . filter ( a => a . length > 2 )
101+ // and remove the last element of each cycle
102+ // because the library spits them out in the form [[0, 1, 0], [0, 1, 2, 4, 3, 0]]
103+ . map ( cycle => cycle . slice ( 0 , - 1 ) )
83104
84- const rawCycles = loops.length > 0 ? loops : findCyclesAdjacency(adjacency)
85-
86- const simpleCycles = rawCycles.map(cycle =>
87- cycle.slice(0, -1).map(i => nodes[i]),
105+ const cycles = [ ...loops , ...otherCycles ] . map ( cycle =>
106+ cycle . map ( i => indexes [ i ] ) ,
88107 )
89108
90- return simpleCycles
91- .map(cycle => simpleCycleToCycle(cycle, dependencies))
92- .sort((a, b) => a.length - b.length)
93- .slice(0, 5)
109+ return cycles
94110}
95111
96- function simpleCycleToCycle(
97- simpleCycle: string[],
98- dependencies: Dependency[],
99- ): Dependency[] {
100- return simpleCycle.map((uri, index) => {
101- const dependent = simpleCycle[index]
102- const dependency = simpleCycle[(index + 1) % simpleCycle.length]
103- return (
104- dependencies.find(
105- d => d.dependency === dependency && d.dependent === dependent,
106- ) || {
107- dependent,
108- dependency,
109- doc: '',
110- }
112+ export const pruneWithCycles = ( graph : Graph ) : [ Graph , UriCycle [ ] ] => {
113+ // clone the graph
114+ graph = cloneDeep ( graph )
115+ // first detect cycles
116+ const cycles = getCycles ( graph )
117+ const edges = cycles
118+ // collect edges from all cycles
119+ . map ( cycle => cycle2edges ( cycle ) )
120+ . flat ( )
121+ // and filter out duplicates
122+ . filter (
123+ ( [ a , b ] , i , edges ) =>
124+ edges . findIndex ( ( [ c , d ] ) => a === c && b === d ) === i ,
111125 )
126+ // then remove all edges that are part of cycles
127+ const dag = removeEdges ( graph , edges )
128+ // then prune the remaining graph
129+ const prunedDag = prune ( dag )
130+ // then add the cycles back
131+ const prunedGraph = addEdges ( prunedDag , edges )
132+ // and return
133+ return [ prunedGraph , cycles ]
134+ }
135+
136+ type Edge = [ string , string ]
137+
138+ export const cycle2edges = ( cycle : UriCycle ) : Edge [ ] =>
139+ cycle . map ( ( uri , i , cycle ) => [ cycle [ i ] , cycle [ ( i + 1 ) % cycle . length ] ] )
140+
141+ const removeEdges = ( graph : Graph , edges : Edge [ ] ) : Graph => {
142+ edges . forEach ( ( [ a , b ] ) => {
143+ delete graph [ a ] . dependencies [ b ]
144+ delete graph [ b ] . dependents [ a ]
112145 } )
113- }
114- */
146+ return graph
147+ }
148+
149+ const addEdges = ( graph : Graph , edges : Edge [ ] ) : Graph => {
150+ edges . forEach ( ( [ a , b ] ) => {
151+ graph [ a ] . dependencies [ b ] = graph [ b ]
152+ graph [ b ] . dependents [ a ] = graph [ a ]
153+ } )
154+ return graph
155+ }
0 commit comments