aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src/graph/dotSource.ts
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <marussy@mit.bme.hu>2023-09-14 19:29:36 +0200
committerLibravatar GitHub <noreply@github.com>2023-09-14 19:29:36 +0200
commit98ed3b6db5f4e51961a161050cc31c66015116e8 (patch)
tree8bfd6d9bc8d6ed23b9eb0f889dd40b6c24fe8f92 /subprojects/frontend/src/graph/dotSource.ts
parentMerge pull request #38 from nagilooh/design-space-exploration (diff)
parentMerge remote-tracking branch 'upstream/main' into partial-interpretation (diff)
downloadrefinery-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.ts366
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
7import type {
8 NodeMetadata,
9 RelationMetadata,
10} from '../xtext/xtextServiceResults';
11
12import type GraphStore from './GraphStore';
13
14const EDGE_WEIGHT = 1;
15const CONTAINMENT_WEIGHT = 5;
16const UNKNOWN_WEIGHT_FACTOR = 0.5;
17
18function 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
28function 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
40interface NodeData {
41 isolated: boolean;
42 exists: string;
43 equalsSelf: string;
44 unaryPredicates: Map<RelationMetadata, string>;
45 count: string;
46}
47
48function 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
122function 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
173function 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
196function 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
226function 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
324function 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
349export 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}