diff options
author | 2023-09-14 19:29:36 +0200 | |
---|---|---|
committer | 2023-09-14 19:29:36 +0200 | |
commit | 98ed3b6db5f4e51961a161050cc31c66015116e8 (patch) | |
tree | 8bfd6d9bc8d6ed23b9eb0f889dd40b6c24fe8f92 /subprojects/frontend/src/graph/dotSource.ts | |
parent | Merge pull request #38 from nagilooh/design-space-exploration (diff) | |
parent | Merge remote-tracking branch 'upstream/main' into partial-interpretation (diff) | |
download | refinery-98ed3b6db5f4e51961a161050cc31c66015116e8.tar.gz refinery-98ed3b6db5f4e51961a161050cc31c66015116e8.tar.zst refinery-98ed3b6db5f4e51961a161050cc31c66015116e8.zip |
Merge pull request #39 from kris7t/partial-interpretation
Implement partial interpretation based model generation
Diffstat (limited to 'subprojects/frontend/src/graph/dotSource.ts')
-rw-r--r-- | subprojects/frontend/src/graph/dotSource.ts | 366 |
1 files changed, 366 insertions, 0 deletions
diff --git a/subprojects/frontend/src/graph/dotSource.ts b/subprojects/frontend/src/graph/dotSource.ts new file mode 100644 index 00000000..bd358dfa --- /dev/null +++ b/subprojects/frontend/src/graph/dotSource.ts | |||
@@ -0,0 +1,366 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | |||
7 | import type { | ||
8 | NodeMetadata, | ||
9 | RelationMetadata, | ||
10 | } from '../xtext/xtextServiceResults'; | ||
11 | |||
12 | import type GraphStore from './GraphStore'; | ||
13 | |||
14 | const EDGE_WEIGHT = 1; | ||
15 | const CONTAINMENT_WEIGHT = 5; | ||
16 | const UNKNOWN_WEIGHT_FACTOR = 0.5; | ||
17 | |||
18 | function nodeName(graph: GraphStore, metadata: NodeMetadata): string { | ||
19 | const name = graph.getName(metadata); | ||
20 | switch (metadata.kind) { | ||
21 | case 'INDIVIDUAL': | ||
22 | return `<b>${name}</b>`; | ||
23 | default: | ||
24 | return name; | ||
25 | } | ||
26 | } | ||
27 | |||
28 | function relationName(graph: GraphStore, metadata: RelationMetadata): string { | ||
29 | const name = graph.getName(metadata); | ||
30 | const { detail } = metadata; | ||
31 | if (detail.type === 'class' && detail.abstractClass) { | ||
32 | return `<i>${name}</i>`; | ||
33 | } | ||
34 | if (detail.type === 'reference' && detail.containment) { | ||
35 | return `<b>${name}</b>`; | ||
36 | } | ||
37 | return name; | ||
38 | } | ||
39 | |||
40 | interface NodeData { | ||
41 | isolated: boolean; | ||
42 | exists: string; | ||
43 | equalsSelf: string; | ||
44 | unaryPredicates: Map<RelationMetadata, string>; | ||
45 | count: string; | ||
46 | } | ||
47 | |||
48 | function computeNodeData(graph: GraphStore): NodeData[] { | ||
49 | const { | ||
50 | semantics: { nodes, relations, partialInterpretation }, | ||
51 | } = graph; | ||
52 | |||
53 | const nodeData = Array.from(Array(nodes.length)).map(() => ({ | ||
54 | isolated: true, | ||
55 | exists: 'FALSE', | ||
56 | equalsSelf: 'FALSE', | ||
57 | unaryPredicates: new Map(), | ||
58 | count: '[0]', | ||
59 | })); | ||
60 | |||
61 | relations.forEach((relation) => { | ||
62 | const visibility = graph.getVisibility(relation.name); | ||
63 | if (visibility === 'none') { | ||
64 | return; | ||
65 | } | ||
66 | const { arity } = relation; | ||
67 | const interpretation = partialInterpretation[relation.name] ?? []; | ||
68 | interpretation.forEach((tuple) => { | ||
69 | const value = tuple[arity]; | ||
70 | if (visibility !== 'all' && value === 'UNKNOWN') { | ||
71 | return; | ||
72 | } | ||
73 | for (let i = 0; i < arity; i += 1) { | ||
74 | const index = tuple[i]; | ||
75 | if (typeof index === 'number') { | ||
76 | const data = nodeData[index]; | ||
77 | if (data !== undefined) { | ||
78 | data.isolated = false; | ||
79 | if (arity === 1) { | ||
80 | data.unaryPredicates.set(relation, value); | ||
81 | } | ||
82 | } | ||
83 | } | ||
84 | } | ||
85 | }); | ||
86 | }); | ||
87 | |||
88 | partialInterpretation['builtin::exists']?.forEach(([index, value]) => { | ||
89 | if (typeof index === 'number' && typeof value === 'string') { | ||
90 | const data = nodeData[index]; | ||
91 | if (data !== undefined) { | ||
92 | data.exists = value; | ||
93 | } | ||
94 | } | ||
95 | }); | ||
96 | |||
97 | partialInterpretation['builtin::equals']?.forEach(([index, other, value]) => { | ||
98 | if ( | ||
99 | typeof index === 'number' && | ||
100 | index === other && | ||
101 | typeof value === 'string' | ||
102 | ) { | ||
103 | const data = nodeData[index]; | ||
104 | if (data !== undefined) { | ||
105 | data.equalsSelf = value; | ||
106 | } | ||
107 | } | ||
108 | }); | ||
109 | |||
110 | partialInterpretation['builtin::count']?.forEach(([index, value]) => { | ||
111 | if (typeof index === 'number' && typeof value === 'string') { | ||
112 | const data = nodeData[index]; | ||
113 | if (data !== undefined) { | ||
114 | data.count = value; | ||
115 | } | ||
116 | } | ||
117 | }); | ||
118 | |||
119 | return nodeData; | ||
120 | } | ||
121 | |||
122 | function createNodes( | ||
123 | graph: GraphStore, | ||
124 | nodeData: NodeData[], | ||
125 | lines: string[], | ||
126 | ): void { | ||
127 | const { | ||
128 | semantics: { nodes }, | ||
129 | scopes, | ||
130 | } = graph; | ||
131 | |||
132 | nodes.forEach((node, i) => { | ||
133 | const data = nodeData[i]; | ||
134 | if (data === undefined || data.isolated || data.exists === 'FALSE') { | ||
135 | return; | ||
136 | } | ||
137 | const classList = [ | ||
138 | `node-${node.kind}`, | ||
139 | `node-exists-${data.exists}`, | ||
140 | `node-equalsSelf-${data.equalsSelf}`, | ||
141 | ]; | ||
142 | if (data.unaryPredicates.size === 0) { | ||
143 | classList.push('node-empty'); | ||
144 | } | ||
145 | const classes = classList.join(' '); | ||
146 | const name = nodeName(graph, node); | ||
147 | const border = node.kind === 'INDIVIDUAL' ? 2 : 1; | ||
148 | const count = scopes ? ` ${data.count}` : ''; | ||
149 | lines.push(`n${i} [id="${node.name}", class="${classes}", label=< | ||
150 | <table border="${border}" cellborder="0" cellspacing="0" style="rounded" bgcolor="white"> | ||
151 | <tr><td cellpadding="4.5" width="32" bgcolor="green">${name}${count}</td></tr>`); | ||
152 | if (data.unaryPredicates.size > 0) { | ||
153 | lines.push( | ||
154 | '<hr/><tr><td cellpadding="4.5"><table fixedsize="TRUE" align="left" border="0" cellborder="0" cellspacing="0" cellpadding="1.5">', | ||
155 | ); | ||
156 | data.unaryPredicates.forEach((value, relation) => { | ||
157 | lines.push( | ||
158 | `<tr> | ||
159 | <td><img src="#${value}"/></td> | ||
160 | <td width="1.5"></td> | ||
161 | <td align="left" href="#${value}" id="${node.name},${ | ||
162 | relation.name | ||
163 | },label">${relationName(graph, relation)}</td> | ||
164 | </tr>`, | ||
165 | ); | ||
166 | }); | ||
167 | lines.push('</table></td></tr>'); | ||
168 | } | ||
169 | lines.push('</table>>]'); | ||
170 | }); | ||
171 | } | ||
172 | |||
173 | function compare( | ||
174 | a: readonly (number | string)[], | ||
175 | b: readonly number[], | ||
176 | ): number { | ||
177 | if (a.length !== b.length + 1) { | ||
178 | throw new Error('Tuple length mismatch'); | ||
179 | } | ||
180 | for (let i = 0; i < b.length; i += 1) { | ||
181 | const aItem = a[i]; | ||
182 | const bItem = b[i]; | ||
183 | if (typeof aItem !== 'number' || typeof bItem !== 'number') { | ||
184 | throw new Error('Invalid tuple'); | ||
185 | } | ||
186 | if (aItem < bItem) { | ||
187 | return -1; | ||
188 | } | ||
189 | if (aItem > bItem) { | ||
190 | return 1; | ||
191 | } | ||
192 | } | ||
193 | return 0; | ||
194 | } | ||
195 | |||
196 | function binarySerach( | ||
197 | tuples: readonly (readonly (number | string)[])[], | ||
198 | key: readonly number[], | ||
199 | ): string | undefined { | ||
200 | let lower = 0; | ||
201 | let upper = tuples.length - 1; | ||
202 | while (lower <= upper) { | ||
203 | const middle = Math.floor((lower + upper) / 2); | ||
204 | const tuple = tuples[middle]; | ||
205 | if (tuple === undefined) { | ||
206 | throw new Error('Range error'); | ||
207 | } | ||
208 | const result = compare(tuple, key); | ||
209 | if (result === 0) { | ||
210 | const found = tuple[key.length]; | ||
211 | if (typeof found !== 'string') { | ||
212 | throw new Error('Invalid tuple value'); | ||
213 | } | ||
214 | return found; | ||
215 | } | ||
216 | if (result < 0) { | ||
217 | lower = middle + 1; | ||
218 | } else { | ||
219 | // result > 0 | ||
220 | upper = middle - 1; | ||
221 | } | ||
222 | } | ||
223 | return undefined; | ||
224 | } | ||
225 | |||
226 | function createRelationEdges( | ||
227 | graph: GraphStore, | ||
228 | nodeData: NodeData[], | ||
229 | relation: RelationMetadata, | ||
230 | showUnknown: boolean, | ||
231 | lines: string[], | ||
232 | ): void { | ||
233 | const { | ||
234 | semantics: { nodes, partialInterpretation }, | ||
235 | } = graph; | ||
236 | const { detail } = relation; | ||
237 | |||
238 | let constraint: 'true' | 'false' = 'true'; | ||
239 | let weight = EDGE_WEIGHT; | ||
240 | let penwidth = 1; | ||
241 | const name = graph.getName(relation); | ||
242 | let label = `"${name}"`; | ||
243 | if (detail.type === 'reference' && detail.containment) { | ||
244 | weight = CONTAINMENT_WEIGHT; | ||
245 | label = `<<b>${name}</b>>`; | ||
246 | penwidth = 2; | ||
247 | } else if ( | ||
248 | detail.type === 'opposite' && | ||
249 | graph.getVisibility(detail.opposite) !== 'none' | ||
250 | ) { | ||
251 | constraint = 'false'; | ||
252 | weight = 0; | ||
253 | } | ||
254 | |||
255 | const tuples = partialInterpretation[relation.name] ?? []; | ||
256 | tuples.forEach(([from, to, value]) => { | ||
257 | const isUnknown = value === 'UNKNOWN'; | ||
258 | if ( | ||
259 | (!showUnknown && isUnknown) || | ||
260 | typeof from !== 'number' || | ||
261 | typeof to !== 'number' || | ||
262 | typeof value !== 'string' | ||
263 | ) { | ||
264 | return; | ||
265 | } | ||
266 | |||
267 | const fromNode = nodes[from]; | ||
268 | const toNode = nodes[to]; | ||
269 | if (fromNode === undefined || toNode === undefined) { | ||
270 | return; | ||
271 | } | ||
272 | |||
273 | const fromData = nodeData[from]; | ||
274 | const toData = nodeData[to]; | ||
275 | if ( | ||
276 | fromData === undefined || | ||
277 | fromData.exists === 'FALSE' || | ||
278 | toData === undefined || | ||
279 | toData.exists === 'FALSE' | ||
280 | ) { | ||
281 | return; | ||
282 | } | ||
283 | |||
284 | let dir = 'forward'; | ||
285 | let edgeConstraint = constraint; | ||
286 | let edgeWeight = weight; | ||
287 | const opposite = binarySerach(tuples, [to, from]); | ||
288 | const oppositeUnknown = opposite === 'UNKNOWN'; | ||
289 | const oppositeSet = opposite !== undefined; | ||
290 | const oppositeVisible = oppositeSet && (showUnknown || !oppositeUnknown); | ||
291 | if (opposite === value) { | ||
292 | if (to < from) { | ||
293 | // We already added this edge in the reverse direction. | ||
294 | return; | ||
295 | } | ||
296 | if (to > from) { | ||
297 | dir = 'both'; | ||
298 | } | ||
299 | } else if (oppositeVisible && to < from) { | ||
300 | // Let the opposite edge drive the graph layout. | ||
301 | edgeConstraint = 'false'; | ||
302 | edgeWeight = 0; | ||
303 | } else if (isUnknown && (!oppositeSet || oppositeUnknown)) { | ||
304 | // Only apply the UNKNOWN value penalty if we aren't the opposite | ||
305 | // edge driving the graph layout from above, or the penalty would | ||
306 | // be applied anyway. | ||
307 | edgeWeight *= UNKNOWN_WEIGHT_FACTOR; | ||
308 | } | ||
309 | |||
310 | lines.push(`n${from} -> n${to} [ | ||
311 | id="${fromNode.name},${toNode.name},${relation.name}", | ||
312 | dir="${dir}", | ||
313 | constraint=${edgeConstraint}, | ||
314 | weight=${edgeWeight}, | ||
315 | xlabel=${label}, | ||
316 | penwidth=${penwidth}, | ||
317 | arrowsize=${penwidth >= 2 ? 0.875 : 1}, | ||
318 | style="${isUnknown ? 'dashed' : 'solid'}", | ||
319 | class="edge-${value}" | ||
320 | ]`); | ||
321 | }); | ||
322 | } | ||
323 | |||
324 | function createEdges( | ||
325 | graph: GraphStore, | ||
326 | nodeData: NodeData[], | ||
327 | lines: string[], | ||
328 | ): void { | ||
329 | const { | ||
330 | semantics: { relations }, | ||
331 | } = graph; | ||
332 | relations.forEach((relation) => { | ||
333 | if (relation.arity !== 2) { | ||
334 | return; | ||
335 | } | ||
336 | const visibility = graph.getVisibility(relation.name); | ||
337 | if (visibility !== 'none') { | ||
338 | createRelationEdges( | ||
339 | graph, | ||
340 | nodeData, | ||
341 | relation, | ||
342 | visibility === 'all', | ||
343 | lines, | ||
344 | ); | ||
345 | } | ||
346 | }); | ||
347 | } | ||
348 | |||
349 | export default function dotSource( | ||
350 | graph: GraphStore | undefined, | ||
351 | ): [string, number] | undefined { | ||
352 | if (graph === undefined) { | ||
353 | return undefined; | ||
354 | } | ||
355 | const lines = [ | ||
356 | 'digraph {', | ||
357 | 'graph [bgcolor=transparent];', | ||
358 | `node [fontsize=12, shape=plain, fontname="OpenSans"];`, | ||
359 | 'edge [fontsize=10.5, color=black, fontname="OpenSans"];', | ||
360 | ]; | ||
361 | const nodeData = computeNodeData(graph); | ||
362 | createNodes(graph, nodeData, lines); | ||
363 | createEdges(graph, nodeData, lines); | ||
364 | lines.push('}'); | ||
365 | return [lines.join('\n'), lines.length]; | ||
366 | } | ||