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, SNode } from "sprotty-protocol";
import { expansionState } from "../../diagram-server.js";
import { Command, Graph, Node, Variable, VerticalEdge } from "../../generated/ast.js";
import { createControlStructureEdge, 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 Down Expand Up @@ -59,11 +59,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, 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 === `dummyinput${csNode.id}`);
Comment thread
Drakae marked this conversation as resolved.
Outdated
return inputDummy ? [inputDummy] : [];
}),
...csNodes,
...generateVerticalCSEdges(controlStructure.nodes, idToSNode, idCache, addMissing, missingFeedback),
//...this.generateHorizontalCSEdges(filteredModel.controlStructure.edges, idCache)
...csNodes.flatMap(csNode => {
const outputDummy = dummyNodes.find(d => d.id === `dummyoutput${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 @@ -356,11 +369,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: "input" | "output",
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 === "input") {
const inputPortId = csNode.id + "_port_input";
Comment thread
Drakae marked this conversation as resolved.
Outdated
// Check if port already exists
if (!csNode.children.find((child: PastaPort) => child.id === inputPortId)) {
csNode.children.push(createPort(inputPortId, PortSide.WEST, assocEdge));
}
}
if (portTypes === "output") {
const outputPortId = csNode.id + "_port_output";
// 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 @@ -384,17 +435,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,
node.level ? node.level - 1 : undefined,
idCache
idCache,
nodeId ?? ""
);
const assocEdgeInput = { node1: inputDummyNode.id ?? "", node2: nodeId ?? "" };
if (nodeId) {
addPortsToNode(nodeId, idToSNode, "input", 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 : "",
inputDummyNode.id ? inputDummyNode.id + "_port" : "",
nodeId ? nodeId + "_port_input" : "",
label,
edgetype,
CS_EDGE_TYPE,
Expand All @@ -407,13 +466,22 @@ export function translateIOToEdgeAndNode(
const outputDummyNode = createDummyNode(
"output" + 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, "output", 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 : "",
nodeId ? nodeId + "_port_output" : "",
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 @@ -186,14 +186,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 @@ -199,7 +199,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"
};
Expand Down