Skip to content

Commit 6e4c8cf

Browse files
andrevidelaepost
authored andcommitted
Decode NLL to BrickDiagram and DiagramInfo (#118, PR #120)
* Add transformation from NLL to Directed graph * Add basic test * delete old file * Rempalce forall and module path * Fix fromNLL * add cyclic check * implement naive transformation to DiagramInfo * Replace enumerate by mapWithIndex * Correctly adjust the width for consecutive elements * update docs and api * Update tests for diagrams encoding * update docs and formatting * Move FromNLL file to Data/Diagram * A few minor formatting/comment edits. #118
1 parent 75bdb42 commit 6e4c8cf

File tree

2 files changed

+197
-4
lines changed

2 files changed

+197
-4
lines changed

src/Data/Diagram/FromNLL.purs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
module View.Diagram.FromNLL (fromNLL, ErrDiagramEncoding(..)) where
2+
3+
import Data.Array hiding (head, tail, length)
4+
import Data.Array.NonEmpty (toNonEmpty)
5+
import Data.Either
6+
import Data.Foldable (length)
7+
import Data.Int (rem)
8+
import Data.Maybe (Maybe(..))
9+
import Data.NonEmpty as NE
10+
import Data.Vec2D (vec3)
11+
import Prelude
12+
import View.Diagram.Model
13+
14+
15+
-- Data types ---------------------------------------------------------------------------------------------------------
16+
17+
-- | This represents a directed graph using only its edges.
18+
-- | Vertices are stored as the source and targets of every edge (or arrow, since it is directed).
19+
-- | Isolated nodes are represented using identity arrows.
20+
-- | This allows representing cyclic graphs and multiple disconnected graphs.
21+
type GraphArrow a =
22+
{ source :: a
23+
, target :: a
24+
}
25+
26+
data ErrDiagramEncoding
27+
= ErrArrayNotRectangular
28+
| ErrGraphIsCyclic
29+
30+
derive instance eqErrorEncodingDiagram :: Eq ErrDiagramEncoding
31+
32+
instance showErrDiagramEncoding :: Show ErrDiagramEncoding where
33+
show ErrArrayNotRectangular = "Error: Brick Diagram is not perfectly square"
34+
show ErrGraphIsCyclic = "Error: Brick Diagram describes a cyclic graph"
35+
36+
type DiagramM a = Either ErrDiagramEncoding a
37+
38+
-- | See https://adoring-curie-7b92fd.netlify.com/.
39+
type BrickDiagram a =
40+
{ width :: Int
41+
, elements :: Array a
42+
}
43+
44+
-- | Makes a brick diagram without checking bounds and sizes.
45+
mkBrickDiagramUnsafe :: a. Int -> Array a -> BrickDiagram a
46+
mkBrickDiagramUnsafe width ops = { width: width, elements: ops }
47+
48+
-- | Safe constructor for brick diagrams, checks that the encoded array has the right size.
49+
mkBrickDiagram :: a. Int -> Array a -> DiagramM (BrickDiagram a)
50+
mkBrickDiagram w ops | length ops `mod` w == 0 = Right $ mkBrickDiagramUnsafe w ops
51+
| otherwise = Left ErrArrayNotRectangular
52+
53+
-- Brick diagram operations -------------------------------------------------------------------------------------------
54+
55+
-- | Get the height of a brick diagram, assuming it's perfectly rectangular.
56+
height :: a. BrickDiagram a -> Int
57+
height b = (length b.elements) / b.width
58+
59+
-- | Return the element below the given coordinates, if any.
60+
below :: a. BrickDiagram a -> Int -> Int -> Maybe a
61+
below b x y = let l = min (height b) (y + 1) in
62+
index b.elements (l * b.width + x)
63+
64+
-- Converting brick diagrams to directed graphs -----------------------------------------------------------------------
65+
66+
-- | Parses a `BrickDiagram` into a directed graph without identity arrows.
67+
-- | This ignores all the nodes which are equal to the first argument.
68+
-- | This also checks that the graph is acyclic.
69+
parseBrickToGraph :: a. Eq a => a -> BrickDiagram a -> DiagramM (Array (GraphArrow a))
70+
parseBrickToGraph empty = checkAcyclic <<< filter (not isSelfArrow) <<< filterNode empty <<< brickToGraph
71+
72+
-- | Same as `parseBrickToGraph` except the `mempty` nodes are ignored
73+
parseBrickToGraphMonoid :: a. Eq a => Monoid a => BrickDiagram a -> DiagramM (Array (GraphArrow a))
74+
parseBrickToGraphMonoid = parseBrickToGraph mempty
75+
76+
-- | Given a node a and a directed graph, remove all arrows which have a as source or dest
77+
filterNode :: a. Eq a => a -> Array (GraphArrow a) -> Array (GraphArrow a)
78+
filterNode value = filter (not nodeContains value)
79+
where
80+
nodeContains :: a -> GraphArrow a -> Boolean
81+
nodeContains v a = a.source == v || a.target == v
82+
83+
-- | Checks if an arrow in the graph has the same source and destination.
84+
isSelfArrow :: a. Eq a => GraphArrow a -> Boolean
85+
isSelfArrow {source, target} = source == target
86+
87+
-- | Map a well formed Brick Diagram to a directed graph.
88+
brickToGraph :: a. Eq a => BrickDiagram a -> Array (GraphArrow a)
89+
brickToGraph b = mapMaybe (edge b) indices
90+
where
91+
indices = range 0 (length b.elements - 1)
92+
93+
-- | Given a brick diagram and an index, return one of the arrows leaving the node at this index.
94+
edge :: a. Eq a => BrickDiagram a -> Int -> Maybe (GraphArrow a)
95+
edge b i = { source: _, target: _ } <$> index b.elements i <*> below b xPos yPos
96+
where
97+
yPos = i / b.width
98+
xPos = i `rem` b.width
99+
100+
-- Converting Directed Graphs to Diagram ------------------------------------------------------------------------------
101+
102+
checkAcyclic :: a. Eq a => Array (GraphArrow a) -> DiagramM (Array (GraphArrow a))
103+
checkAcyclic graph = if isAcyclic graph then Right graph else Left ErrGraphIsCyclic
104+
105+
isAcyclic :: a. Eq a => Array (GraphArrow a) -> Boolean
106+
isAcyclic graph = case uncons graph of
107+
Just {head, tail} -> isAcyclicHelper [] head.source graph
108+
Nothing -> true
109+
110+
isAcyclicHelper :: a. Eq a => Array a -> a -> Array (GraphArrow a) -> Boolean
111+
isAcyclicHelper seen current graph = let targets = getTargets current graph in
112+
all (\t -> (t `not elem` seen) && isAcyclicHelper (t : seen) t graph) targets
113+
114+
-- | Given a node in a graph and the arrows in the graph, return the arrows which have
115+
-- | the node as source.
116+
getSources :: a. Eq a => a -> Array (GraphArrow a) -> Array (GraphArrow a)
117+
getSources node = filter (\a -> a.source == node)
118+
119+
-- | Given a node and the arrows in the graph, return all the nodes which are direct
120+
-- | targets of the node.
121+
getTargets :: a. Eq a => a -> Array (GraphArrow a) -> Array a
122+
getTargets node = map _.target <<< getSources node
123+
124+
-- Converting naively from BrickDiagram to DiagramInfo ----------------------------------------------------------------
125+
126+
brickToDiagram :: a. Eq a => Show a => BrickDiagram a -> String -> DiagramInfo
127+
brickToDiagram brick name = { name: name, ops: graphToOps brick }
128+
129+
type ConsecutiveValues a = { value :: a, length :: Int }
130+
131+
-- | This function assumes the brick diagram in argument encodes the information to lay out the diagram, e.g.:
132+
-- | ```
133+
-- | 2
134+
-- | 23
135+
-- | 11
136+
-- | 40
137+
-- | ```
138+
-- | will display the diagram:
139+
-- | ```
140+
-- | [--2--] [--3--]
141+
-- | [------1------]
142+
-- | [--4--]
143+
-- | ```
144+
graphToOps :: a. Eq a => Show a => BrickDiagram a -> Array Operator
145+
graphToOps { width: width, elements: brick } =
146+
let lines = splitLines width brick
147+
l = mapWithIndex mapOperators $ map packConsecutive lines in
148+
concat l
149+
where
150+
splitLines :: Int -> Array a -> Array (Array a)
151+
splitLines w array | length array <= w = [array]
152+
| otherwise = take width array : splitLines width (drop width array)
153+
154+
packConsecutive :: Array a -> Array (ConsecutiveValues a)
155+
packConsecutive = map ((\n -> { value: NE.head n, length: length n }) <<< toNonEmpty) <<< groupBy (==)
156+
157+
mapOperators :: Int -> Array (ConsecutiveValues a) -> Array Operator
158+
mapOperators row line = mapWithIndex (mkOperator row) line
159+
160+
mkOperator :: Int -> Int -> ConsecutiveValues a -> Operator
161+
mkOperator row col value =
162+
{ identifier: show row <> ":" <> show col
163+
, pos: vec3 row col value.length
164+
, label: show value.value
165+
}
166+
167+
-- Converting from NLL ------------------------------------------------------------------------------------------------
168+
169+
nllToBrickDiagram :: Array Int -> DiagramM (BrickDiagram Int)
170+
nllToBrickDiagram nll = case uncons nll of
171+
Just { head, tail } -> mkBrickDiagram head tail
172+
Nothing -> Right $ mkBrickDiagramUnsafe 0 []
173+
174+
fromNLL :: Array Int -> String -> DiagramM DiagramInfo
175+
fromNLL nll name = do bricks <- nllToBrickDiagram nll
176+
_ <- parseBrickToGraph 0 bricks
177+
Right $ brickToDiagram bricks name

test/Main.purs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@ module Test.Main where
33
import Prelude
44

55
import Data.Either (Either(..))
6+
import Data.Petrinet.Representation.NLL (ErrNetEncoding(..))
7+
import Data.Petrinet.Representation.NLL as Net
68
import Data.Tuple (Tuple(..))
9+
import Data.Vec2D (vec3)
710
import Effect (Effect)
811
import Effect.Class.Console (log)
9-
1012
import Test.Spec (pending, describe, it)
1113
import Test.Spec.Assertions (shouldEqual)
1214
import Test.Spec.Reporter.Console (consoleReporter)
1315
import Test.Spec.Runner (run)
14-
15-
import Data.Petrinet.Representation.NLL as Net
16-
import Data.Petrinet.Representation.NLL (ErrNetEncoding(..))
16+
import View.Diagram.FromNLL as Diagram
1717

1818
main :: Effect Unit
1919
main = run [consoleReporter] do
@@ -23,3 +23,19 @@ main = run [consoleReporter] do
2323
it "reject odd length encodings" do
2424
Net.fromNLL 0 [1,2,0,3,0,3,0] `shouldEqual` Left ErrOddLength
2525
pending "infer a single trailing zero?"
26+
describe "Brick diagram encoding" do
27+
it "decodes a simple graph" do
28+
Diagram.fromNLL [2, 2, 1, 8, 8, 8, 3] "test" `shouldEqual`
29+
Right { name: "test"
30+
, ops: [ { identifier: "0:0", pos: vec3 0 0 1, label: "2" }
31+
, { identifier: "0:1", pos: vec3 0 1 1, label: "1" }
32+
, { identifier: "1:0", pos: vec3 1 0 2, label: "8" }
33+
, { identifier: "2:0", pos: vec3 2 0 1, label: "8" }
34+
, { identifier: "2:1", pos: vec3 2 1 1, label: "3" }
35+
]
36+
}
37+
38+
it "should fail on cyclic graphs" do
39+
Diagram.fromNLL [1, 1,2,3,1] "test" `shouldEqual` Left Diagram.ErrGraphIsCyclic
40+
-- it "should fail on multiple graphs with one cyclic" do
41+
-- Diag.fromNLL [2,2,5,3,5,4,6,0,5] "test" `shouldEqual` Left Diag.ErrGraphIsCyclic

0 commit comments

Comments
 (0)