Skip to content

Commit

Permalink
added option to show missing feedback in the control structure
Browse files Browse the repository at this point in the history
  • Loading branch information
Drakae committed Oct 9, 2024
1 parent 6e28cdb commit 761b054
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 47 deletions.
139 changes: 99 additions & 40 deletions extension/src-language-server/stpa/diagram/diagram-controlStructure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,17 @@ import { getCommonAncestor, setLevelOfCSNodes, sortPorts } from "./utils";
* @param idToSNode The map of IDs to SNodes.
* @param options The synthesis options of the STPA model.
* @param idCache The ID cache of the STPA model.
* @param addMissing Whether missing feedback should be added to the control structure.
* @param missingFeedback The missing feedbacks of the control structure.
* @returns the generated control structure diagram.
*/
export function createControlStructure(
controlStructure: Graph,
idToSNode: Map<string, SNode>,
options: StpaSynthesisOptions,
idCache: IdCache<AstNode>
idCache: IdCache<AstNode>,
addMissing: boolean,
missingFeedback?: Map<string, Node[]>
): ParentNode {
// set the level of the nodes in the control structure automatically
setLevelOfCSNodes(controlStructure.nodes);
Expand All @@ -56,7 +60,7 @@ export function createControlStructure(
// children (nodes and edges) of the control structure
const CSChildren = [
...csNodes,
...generateVerticalCSEdges(controlStructure.nodes, idToSNode, idCache),
...generateVerticalCSEdges(controlStructure.nodes, idToSNode, idCache, addMissing, missingFeedback),
//...this.generateHorizontalCSEdges(filteredModel.controlStructure.edges, idCache)
];
// sort the ports in order to group edges based on the nodes they are connected to
Expand Down Expand Up @@ -179,47 +183,67 @@ export function createProcessModelNodes(variables: Variable[], idCache: IdCache<
* Creates the edges for the control structure.
* @param nodes The nodes of the control structure.
* @param idCache The ID cache of the STPA model.
* @param addMissing Whether missing feedback should be added to the control structure.
* @param missingFeedback The missing feedbacks of the control structure.
* @returns A list of edges for the control structure.
*/
export function generateVerticalCSEdges(
nodes: Node[],
idToSNode: Map<string, SNode>,
idCache: IdCache<AstNode>
idCache: IdCache<AstNode>,
addMissing: boolean,
missingFeedback?: Map<string, Node[]>
): (CSNode | CSEdge)[] {
const edges: (CSNode | CSEdge)[] = [];
// for every control action and feedback of every a node, a edge should be created
for (const node of nodes) {
// create edges representing the control actions
edges.push(...translateCommandsToEdges(node.actions, EdgeType.CONTROL_ACTION, idToSNode, idCache));
edges.push(
...translateCommandsToEdges(
node,
node.actions,
EdgeType.CONTROL_ACTION,
idToSNode,
idCache,
addMissing,
missingFeedback
)
);
// create edges representing feedback
edges.push(...translateCommandsToEdges(node.feedbacks, EdgeType.FEEDBACK, idToSNode, idCache));
edges.push(...translateCommandsToEdges(node, node.feedbacks, EdgeType.FEEDBACK, idToSNode, idCache, false));
// create edges representing the other inputs
edges.push(...translateIOToEdgeAndNode(node.inputs, node, EdgeType.INPUT, idToSNode, idCache));
// create edges representing the other outputs
edges.push(...translateIOToEdgeAndNode(node.outputs, node, EdgeType.OUTPUT, idToSNode, idCache));
// create edges for children and add the ones that must be added at the top level
edges.push(...generateVerticalCSEdges(node.children, idToSNode, idCache));
edges.push(...generateVerticalCSEdges(node.children, idToSNode, idCache, addMissing, missingFeedback));
}
return edges;
}

/**
* Translates the commands (control action or feedback) of a node to (intermediate) edges and adds them to the correct nodes.
* @param node The node of the commands.
* @param commands The control actions or feedback of a node.
* @param edgeType The type of the edge (control action or feedback).
* @param idToSNode The map of IDs to SNodes.
* @param idCache The ID cache of the STPA model.
* @param addMissing Whether missing feedback should be added to the control structure.
* @param missingFeedback The missing feedbacks of the control structure.
* @returns A list of edges representing the commands that should be added at the top level.
*/
export function translateCommandsToEdges(
source: Node,
commands: VerticalEdge[],
edgeType: EdgeType,
idToSNode: Map<string, SNode>,
idCache: IdCache<AstNode>
idCache: IdCache<AstNode>,
addMissing: boolean,
missingFeedback?: Map<string, Node[]>
): CSEdge[] {
const edges: CSEdge[] = [];
for (const edge of commands) {
// create edge id
const source = edge.$container;
const target = edge.target.ref;
const edgeId = idCache.uniqueId(
`${idCache.getId(source)}_${edge.comms[0].name}_${idCache.getId(target)}`,
Expand All @@ -233,44 +257,79 @@ export function translateCommandsToEdges(
const com = edge.comms[i];
label.push(com.label);
}
// edges can be hierachy crossing so we must determine the common ancestor of source and target
const commonAncestor = getCommonAncestor(source, target);
// create the intermediate ports and edges
const ports = generateIntermediateCSEdges(
source,
target,
edgeId,
edgeType,
idToSNode,
idCache,
commonAncestor
);
// add edge between the two ports in the common ancestor
const csEdge = createControlStructureEdge(
idCache.uniqueId(edgeId),
ports.sourcePort,
ports.targetPort,
label,
edgeType,
// if the common ancestor is the parent of the target we want an edge with an arrow otherwise an intermediate edge
target.$container === commonAncestor ? CS_EDGE_TYPE : CS_INTERMEDIATE_EDGE_TYPE,
idCache
);
if (commonAncestor?.$type === "Graph") {
// if the common ancestor is the graph, the edge must be added at the top level and hence have to be returned
edges.push(csEdge);
} else if (commonAncestor) {
// if the common ancestor is a node, the edge must be added to the children of the common ancestor
const snodeAncestor = idToSNode.get(idCache.getId(commonAncestor)!);
snodeAncestor?.children
?.find(node => node.type === CS_INVISIBLE_SUBCOMPONENT_TYPE)
?.children?.push(csEdge);
createEdgeForCommand(source, target, edgeId, edgeType, label, idToSNode, idCache, edges);
}
}

// add missing feedback edges
if (addMissing && missingFeedback) {
// add feedback edge to each node to which a feedback is missing
for (const target of missingFeedback.get(source?.name ?? "") ?? []) {
if (source && target) {
createEdgeForCommand(
source,
target,
idCache.uniqueId(`${source?.name}_missingFeedback_${target}`),
EdgeType.FEEDBACK,
// TODO: mark edge as missing to render it red
["MISSING FEEDBACK"],
idToSNode,
idCache,
edges
);
}
}
}

return edges;
}

/**
* Creates (intermediate) edges for the given {@code source} and {@code target} and adds them to the correct node.
* @param source The source of the edge.
* @param target The target of the edge.
* @param edgeId The ID of the edge.
* @param edgeType The type of the edge.
* @param label The label of the edge.
* @param idToSNode The map of IDs to SNodes.
* @param idCache The ID cache of the STPA model.
* @param edges The list of edges to add the created edges to.
*/
export function createEdgeForCommand(
source: Node,
target: Node,
edgeId: string,
edgeType: EdgeType,
label: string[],
idToSNode: Map<string, SNode>,
idCache: IdCache<AstNode>,
edges: CSEdge[]
): void {
// edges can be hierachy crossing so we must determine the common ancestor of source and target
const commonAncestor = getCommonAncestor(source, target);
// create the intermediate ports and edges
const ports = generateIntermediateCSEdges(source, target, edgeId, edgeType, idToSNode, idCache, commonAncestor);
// add edge between the two ports in the common ancestor
const csEdge = createControlStructureEdge(
idCache.uniqueId(edgeId),
ports.sourcePort,
ports.targetPort,
label,
edgeType,
// if the common ancestor is the parent of the target we want an edge with an arrow otherwise an intermediate edge
target.$container === commonAncestor ? CS_EDGE_TYPE : CS_INTERMEDIATE_EDGE_TYPE,
idCache
);
if (commonAncestor?.$type === "Graph") {
// if the common ancestor is the graph, the edge must be added at the top level and hence have to be returned
edges.push(csEdge);
} else if (commonAncestor) {
// if the common ancestor is a node, the edge must be added to the children of the common ancestor
const snodeAncestor = idToSNode.get(idCache.getId(commonAncestor)!);
snodeAncestor?.children?.find(node => node.type === CS_INVISIBLE_SUBCOMPONENT_TYPE)?.children?.push(csEdge);
}
}

/**
* Translates the inputs or outputs of a node to edges.
* @param io The inputs or outputs of a node.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,14 @@ export class StpaDiagramGenerator extends SnippetGraphGenerator {
if (filteredModel.controlStructure) {
// add control structure to roots children
rootChildren.push(
createControlStructure(filteredModel.controlStructure, this.idToSNode, this.options, this.idCache)
createControlStructure(
filteredModel.controlStructure,
this.idToSNode,
this.options,
this.idCache,
this.options.getShowUnclosedFeedbackLoopsOption(),
this.services.validation.StpaValidator.missingFeedback
)
);
}
// add relationship graph to roots children
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const filterCategoryID = "filterCategory";

const showControlStructureID = "showControlStructure";
const showProcessModelsID = "showProcessModels";
const showUnclosedFeedbackLoopsID = "showUnclosedFeedbackLoops";
const showRelationshipGraphID = "showRelationshipGraph";

/**
Expand Down Expand Up @@ -312,6 +313,22 @@ const showLabelsOption: ValuedSynthesisOption = {
currentValue: "automatic",
};

/**
* Boolean option to toggle the visualization of missing feedback in the control structure.
*/
const showUnclosedFeedbackLoopsOption: ValuedSynthesisOption = {
synthesisOption: {
id: showUnclosedFeedbackLoopsID,
name: "Missing Feedback Loops",
type: TransformationOptionType.CHECK,
initialValue: true,
currentValue: true,
values: [true, false],
category: filterCategory,
},
currentValue: true,
};

/**
* Values for filtering the node labels.
*/
Expand Down Expand Up @@ -340,6 +357,7 @@ export class StpaSynthesisOptions extends SynthesisOptions {
filteringOfUCAs,
showControlStructureOption,
showProcessModelsOption,
showUnclosedFeedbackLoopsOption,
showRelationshipGraphOption,
showSysConsOption,
showRespsOption,
Expand Down Expand Up @@ -511,6 +529,14 @@ export class StpaSynthesisOptions extends SynthesisOptions {
return this.getOption(showSafetyConstraintsID)?.currentValue;
}

setShowUnclosedFeedbackLoops(value: boolean): void {
this.setOption(showUnclosedFeedbackLoopsID, value);
}

getShowUnclosedFeedbackLoopsOption(): boolean {
return this.getOption(showUnclosedFeedbackLoopsID)?.currentValue;
}

/**
* Updates the filterUCAs option with the availabe cotrol actions.
* @param values The currently avaiable control actions.
Expand Down
48 changes: 42 additions & 6 deletions extension/src-language-server/stpa/services/stpa-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@ import { Position } from "vscode-languageserver-types";
import {
Context,
ControllerConstraint,
DCAContext,
DCARule,
Graph,
Hazard,
HazardList,
Loss,
Model,
Node,
Responsibility,
PastaAstType,
Responsibility,
Rule,
SystemConstraint,
isModel,
Graph,
Rule,
DCARule,
DCAContext,
isRule,
} from "../../generated/ast";
import { StpaServices } from "../stpa-module";
Expand Down Expand Up @@ -77,6 +77,11 @@ export class StpaValidator {

checkForConflictingUCAs = true;

/**
* Map from node ID to a list of nodes to which a feedback is missing.
*/
missingFeedback: Map<string, Node[]> = new Map();

/**
* Executes validation checks for the whole model.
* @param model The model to validate.
Expand Down Expand Up @@ -434,6 +439,37 @@ export class StpaValidator {
checkControlStructure(graph: Graph, accept: ValidationAcceptor): void {
const nodes = [...graph.nodes, ...graph.nodes.map(node => this.getChildren(node)).flat(1)];
this.checkIDsAreUnique(nodes, accept);
this.checkForMissingFeedback(nodes, accept);
}

/**
* Checks whether feedback is missing in the control structure and fills the missingFeedback map.
* @param nodes The nodes of the control structure.
* @param accept
*/
protected checkForMissingFeedback(nodes: Node[], accept: ValidationAcceptor): void {
// TODO: show missing feedback as warning in editor??
this.missingFeedback.clear();
for (const node of nodes) {
const nodeID = node.name;
// check for each action of the node whether feedback is missing
node.actions.forEach(action => {
const target = action.target.ref;
if (target) {
// check if target sents feedback back
const sentFeedback = target.feedbacks.find(feedback => feedback.target.$refText === nodeID);
if (!sentFeedback) {
// add the missing feedback to the map
const targetID = target.name;
if (!this.missingFeedback.has(targetID)) {
this.missingFeedback.set(targetID, [node]);
} else {
this.missingFeedback.get(targetID)?.push(node);
}
}
}
});
}
}

/**
Expand Down Expand Up @@ -559,7 +595,7 @@ export class StpaValidator {
/**
* Checks whether the model contains any TODOs.
* @param model The model to check.
* @param accept
* @param accept
*/
protected checkForTODOs(model: Model, accept: ValidationAcceptor): void {
model.losses.forEach(loss => {
Expand Down

0 comments on commit 761b054

Please sign in to comment.