Skip to content
5 changes: 2 additions & 3 deletions extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ SC1 "Vessel must not be exposed to major damage or breakdown" [H1] {
ControlStructure
Ferry {
ControlCentre {
hierarchyLevel 0
processModel {
mode: [docking, driving]
}
Expand All @@ -121,7 +120,6 @@ Ferry {
}
}
VirtualCaptain {
hierarchyLevel 1
controlActions {
[pars "Set parameters"] -> Engine
}
Expand All @@ -130,7 +128,6 @@ Ferry {
}
}
Engine {
hierarchyLevel 2
feedback {
[motion "Motion"] -> VirtualCaptain
}
Expand Down Expand Up @@ -177,6 +174,8 @@ RL1 {
}
}
```
*Note*
The keyword `hierarchyLevel` is deprecated and is no longer supported. The arrangement of the nodes in the different levels of the relationship graph is now determined using the model order.

### FTA

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { SModelElement, SLabel, SNode, HAlignment } from "sprotty-protocol";
import { expansionState } from "../../diagram-server.js";
import { Command, Graph, Node, Variable, VerticalEdge } from "../../generated/ast.js";
import { createControlStructureEdge, createInvisibleProcessModelEdge, createDummyNode, createLabel, createPort } from "./diagram-elements.js";
import { CSEdge, CSNode, ParentNode } from "./stpa-interfaces.js";
import { CSEdge, CSNode, ParentNode, PastaPort } from "./stpa-interfaces.js";
import {
CS_EDGE_TYPE,
CS_INTERMEDIATE_EDGE_TYPE,
Expand All @@ -37,6 +37,9 @@ import {
import { StpaSynthesisOptions } from "./stpa-synthesis-options.js";
import { getCommonAncestor, sortPorts } from "./utils.js";

const inputID = "input" as const;
const outputID = "output" as const;

/**
* Creates the control structure diagram for the given {@code controlStructure}.
* @param controlStructure The control structure.
Expand All @@ -61,11 +64,24 @@ export function createControlStructure(
// setLevelOfCSNodes(controlStructure.nodes);
// determine the nodes of the control structure graph
const csNodes = controlStructure.nodes.map(n => createControlStructureNode(n, idToSNode, options, idCache));
// children (nodes and edges) of the control structure

// get all edges of graph (plus dummyNodes for input/output)
const verticalEdgesAndDummyNodes = generateVerticalCSEdges(controlStructure.nodes, idToSNode, idCache, missingReferences, addMissing, missingFeedback);
const dummyNodes = verticalEdgesAndDummyNodes.filter(item => item.type === "node:dummy");
const verticalEdges = verticalEdgesAndDummyNodes.filter(item => item.type !== "node:dummy");

// children of the control structure (first input dummys, then nodes / edges, last output dummys)
const CSChildren = [
...csNodes.flatMap(csNode => {
const inputDummy = dummyNodes.find(d => d.id === `dummy${inputID}${csNode.id}`);
return inputDummy ? [inputDummy] : [];
}),
...csNodes,
...generateVerticalCSEdges(controlStructure.nodes, idToSNode, idCache, missingReferences, addMissing, missingFeedback),
//...this.generateHorizontalCSEdges(filteredModel.controlStructure.edges, idCache)
...csNodes.flatMap(csNode => {
const outputDummy = dummyNodes.find(d => d.id === `dummy${outputID}${csNode.id}`);
return outputDummy ? [outputDummy] : [];
}),
...verticalEdges,
];
// sort the ports in order to group edges based on the nodes they are connected to
sortPorts(CSChildren.filter(node => node.type.startsWith("node")) as CSNode[]);
Expand Down Expand Up @@ -358,11 +374,49 @@ export function createEdgeForCommand(
}
}

/**
* Creates input / output ports for a given node.
* @param nodeId The ID of the node.
* @param idToSNode The map of IDs to SNodes.
* @param portTypes Shows if port should be input or output
* @param assocEdge The associated edge for which the ports should be created.
*/
function addPortsToNode(
nodeId: string,
idToSNode: Map<string, any>,
portTypes: typeof inputID | typeof outputID,
assocEdge: { node1: string; node2: string }
): void {
const csNode = idToSNode.get(nodeId);
if (csNode) {
// Initialize children array if it doesn't exist
if (!csNode.children) {
csNode.children = [];
}

if (portTypes === inputID) {
const inputPortId = csNode.id + `_port_${inputID}`;
// Check if port already exists
if (!csNode.children.find((child: PastaPort) => child.id === inputPortId)) {
csNode.children.push(createPort(inputPortId, PortSide.WEST, assocEdge));
}
}
if (portTypes === outputID) {
const outputPortId = csNode.id + `_port_${outputID}`;
// Check if port already exists
if (!csNode.children.find((child: PastaPort) => child.id === outputPortId)) {
csNode.children.push(createPort(outputPortId, PortSide.EAST, assocEdge));
}
}
}
}

/**
* Translates the inputs or outputs of a node to edges.
* @param io The inputs or outputs of a node.
* @param node The node of the inputs or outputs.
* @param edgetype The type of the edge (input or output).
* @param idToSNode The map of IDs to SNodes.
* @param idCache The ID cache of the STPA model.
* @returns a list of edges representing the inputs or outputs that should be added at the top level.
*/
Expand All @@ -386,17 +440,25 @@ export function translateIOToEdgeAndNode(
let graphComponents: (CSNode | CSEdge)[] = [];
switch (edgetype) {
case EdgeType.INPUT:
// create input port for node
// create dummy node for the input
const inputDummyNode = createDummyNode(
"input" + node.name,
inputID + node.name,
node.level ? node.level - 1 : undefined,
idCache
idCache,
nodeId ?? ""
);
const assocEdgeInput = { node1: inputDummyNode.id ?? "", node2: nodeId ?? "" };
if (nodeId) {
addPortsToNode(nodeId, idToSNode, inputID, assocEdgeInput);
} else {
console.error("No node with ID in cache");
}
// create edge for the input
const inputEdge = createControlStructureEdge(
idCache.uniqueId(`${inputDummyNode.id}_input_${nodeId}`),
inputDummyNode.id ? inputDummyNode.id : "",
nodeId ? nodeId : "",
idCache.uniqueId(`${inputDummyNode.id}_${inputID}_${nodeId}`),
inputDummyNode.id ? inputDummyNode.id + "_port" : "",
nodeId ? nodeId + `_port_${inputID}` : "",
label,
edgetype,
CS_EDGE_TYPE,
Expand All @@ -407,15 +469,24 @@ export function translateIOToEdgeAndNode(
case EdgeType.OUTPUT:
// create dummy node for the output
const outputDummyNode = createDummyNode(
"output" + node.name,
outputID + node.name,
node.level ? node.level + 1 : undefined,
idCache
idCache,
nodeId ?? ""
);
const assocEdgeOutput = { node1: nodeId ?? "", node2: outputDummyNode.id ?? "" };
// create output port for node
if (nodeId) {
addPortsToNode(nodeId, idToSNode, outputID, assocEdgeOutput);
} else {
console.error("No node with ID in cache");
}

// create edge for the output
const outputEdge = createControlStructureEdge(
idCache.uniqueId(`${nodeId}_output_${outputDummyNode.id}`),
nodeId ? nodeId : "",
outputDummyNode.id ? outputDummyNode.id : "",
idCache.uniqueId(`${nodeId}_${outputID}_${outputDummyNode.id}`),
nodeId ? nodeId + `_port_${outputID}` : "",
outputDummyNode.id ? outputDummyNode.id + "_port" : "",
label,
edgetype,
CS_EDGE_TYPE,
Expand Down
11 changes: 9 additions & 2 deletions extension/src-language-server/stpa/diagram/diagram-elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,14 +235,21 @@ export function createLabel(
* Creates a dummy node.
* @param idCache The ID cache of the STPA model.
* @param level The level of the dummy node.
* @param assocNode The associated node of the input / output. (Is either source or target of the connected edge.)
* @returns a dummy node.
*/
export function createDummyNode(name: string, level: number | undefined, idCache: IdCache<AstNode>): CSNode {
export function createDummyNode(name: string, level: number | undefined, idCache: IdCache<AstNode>, assocNode: string): CSNode {
const isInput = name.startsWith("input");
const assocEdge = {
node1: isInput ? name : assocNode,
node2: isInput ? assocNode : name
};
const dummyNode: CSNode = {
type: DUMMY_NODE_TYPE,
id: idCache.uniqueId("dummy" + name),
layout: "stack",
hasChildren: false,
hasChildren: true,
children: [createPort(idCache.uniqueId("dummy" + name + "_port"), isInput ? PortSide.SOUTH : PortSide.NORTH, assocEdge)],
expanded: false,
layoutOptions: {
paddingTop: 10.0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator {
// nodes with many edges are streched
"org.eclipse.elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
"org.eclipse.elk.layered.nodePlacement.networkSimplex.nodeFlexibility.default": "NODE_SIZE",
"org.eclipse.elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES",
"org.eclipse.elk.layered.considerModelOrder.strategy": "PREFER_NODES",
"org.eclipse.elk.layered.crossingMinimization.forceNodeModelOrder": "true",
"org.eclipse.elk.layered.cycleBreaking.strategy": "MODEL_ORDER",
"org.eclipse.elk.separateConnectedComponents": "false"
Expand Down