Skip to content

Commit

Permalink
support operator output with multiple types (#4200)
Browse files Browse the repository at this point in the history
Co-authored-by: Yohann Paris <[email protected]>
Co-authored-by: Shawn Yama <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
4 people authored Jul 22, 2024
1 parent ac908ad commit 62e0c29
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 9 deletions.
58 changes: 51 additions & 7 deletions packages/client/hmi-client/src/services/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,38 @@ export const addNode = (
wf.nodes.push(node);
};

/**
* Create an edge between two nodes, and transfer the value from
* source-node's output port to target-node's input port
*
* The terms are used as depicted in the following schematics
*
*
* ------------ ------------
* | Source | | Target |
* | | | |
* | [output port] ---> [input port] |
* | | | |
* ------------ -------------
*
*
* There are several special cases
*
* The target-input can accept multiple types, but this is more of an exception
* rather than the norm. This is resolved by checking one of the accepted type
* matches the provided type.
*
* The second case is when the source produce multiple types, a similar check, but
* on the reverse side is performed.
*
*
* Ambiguity arises when the source provide multiple types and the target accepts multiple
* types and there is no resolution. For example
* - source provides {A, B, C}
* - target accepts A or C
* We do not deal with this and throw an error/warning, and the edge creation will be cancelled.
*
* */
export const addEdge = (
wf: Workflow,
sourceId: string,
Expand Down Expand Up @@ -135,26 +167,38 @@ export const addEdge = (
if (existingEdge) return;

// Check if type is compatible
const allowedTypes = targetInputPort.type.split('|');
const outputTypes = sourceOutputPort.type.split('|');
const allowedInputTypes = targetInputPort.type.split('|');
const intersectionTypes = _.intersection(outputTypes, allowedInputTypes);

// Not supported if there are more than one match
if (intersectionTypes.length > 1) {
console.error(`Ambiguous matching types [${outputTypes}] to [${allowedInputTypes}]`);
return;
}

// Not supported if there is a mismatch
if (
!allowedTypes.includes(sourceOutputPort.type) ||
intersectionTypes.length === 0 ||
(!targetInputPort.acceptMultiple && targetInputPort.status === WorkflowPortStatus.CONNECTED)
) {
return;
}

// Transfer data value/reference
if (targetInputPort.acceptMultiple && targetInputPort.value && sourceOutputPort.value) {
targetInputPort.label = `${sourceOutputPort.label},${targetInputPort.label}`;
targetInputPort.value = [...targetInputPort.value, ...sourceOutputPort.value];
targetInputPort.label = sourceOutputPort.label;
if (outputTypes.length > 1) {
const concreteType = intersectionTypes[0];
if (sourceOutputPort.value) {
targetInputPort.value = [sourceOutputPort.value[0][concreteType]];
}
} else {
targetInputPort.label = sourceOutputPort.label;
targetInputPort.value = sourceOutputPort.value;
}

// Transfer concrete type to the input type to match the output type
// Saves the original type in case we want to revert when we unlink the edge
if (allowedTypes.length > 1) {
if (allowedInputTypes.length > 1) {
targetInputPort.originalType = targetInputPort.type;
targetInputPort.type = sourceOutputPort.type;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/client/hmi-client/src/types/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export interface OperationData {
type: string;
label?: string;
isOptional?: boolean;
acceptMultiple?: boolean;
acceptMultiple?: boolean; // @deprecated
}

// Defines a function: eg: model, simulate, calibrate
Expand Down Expand Up @@ -81,7 +81,7 @@ export interface WorkflowPort {
label?: string;
value?: any[] | null;
isOptional: boolean;
acceptMultiple?: boolean;
acceptMultiple?: boolean; // @deprecated
}

// Operator Output needs more information than a standard operator port.
Expand Down
110 changes: 110 additions & 0 deletions packages/client/hmi-client/tests/unit/services/workflow.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,113 @@ describe('workflow copying branch >- fork', () => {
expect(sanityCheck(wf)).to.eq(true);
});
});

describe('workflow operator with multiple output types', () => {
const commonFields = {
name: 'test' as any,
displayName: 'test',
description: 'test',
isRunnable: true
};

const multiOutputOp: Operation = {
...commonFields,
inputs: [],
outputs: [{ type: 'datasetId|modelId' }]
};

const datasetOp: Operation = {
...commonFields,
inputs: [{ type: 'datasetId' }],
outputs: []
};

const modelConfigOp: Operation = {
...commonFields,
inputs: [{ type: 'modelId' }],
outputs: []
};

const edgeCaseOp: Operation = {
...commonFields,
inputs: [{ type: 'modelId|datasetId|number' }],
outputs: []
};

const wf = workflowService.emptyWorkflow('test', 'test');
workflowService.addNode(wf, multiOutputOp, { x: 0, y: 0 }, {});
workflowService.addNode(wf, datasetOp, { x: 0, y: 0 }, {});
workflowService.addNode(wf, modelConfigOp, { x: 0, y: 0 }, {});
workflowService.addNode(wf, testOp, { x: 0, y: 0 }, {});
workflowService.addNode(wf, edgeCaseOp, { x: 0, y: 0 }, {});

const multiOutputNode = wf.nodes[0];
multiOutputNode.outputs[0].value = [
{
datasetId: 'dataset xyz',
modelId: 'model abc'
}
];

const datasetNode = wf.nodes[1];
const modelNode = wf.nodes[2];
const testNode = wf.nodes[3];
const edgeCaseNode = wf.nodes[4];

it('dataset|model => dataset', () => {
workflowService.addEdge(
wf,
multiOutputNode.id,
multiOutputNode.outputs[0].id,
datasetNode.id,
datasetNode.inputs[0].id,
[]
);

expect(datasetNode.inputs[0].value).toMatchObject(['dataset xyz']);
expect(wf.edges.length).eq(1);
workflowService.removeEdge(wf, wf.edges[0].id);
});

it('dataset|model => model', () => {
workflowService.addEdge(
wf,
multiOutputNode.id,
multiOutputNode.outputs[0].id,
modelNode.id,
modelNode.inputs[0].id,
[]
);

expect(modelNode.inputs[0].value).toMatchObject(['model abc']);
expect(wf.edges.length).eq(1);
workflowService.removeEdge(wf, wf.edges[0].id);
});

it('dataset|model => test', () => {
workflowService.addEdge(
wf,
multiOutputNode.id,
multiOutputNode.outputs[0].id,
testNode.id,
testNode.inputs[0].id,
[]
);

expect(testNode.inputs[0].value).toBeNull();
expect(wf.edges.length).eq(0);
});

it('edge case many to many', () => {
workflowService.addEdge(
wf,
multiOutputNode.id,
multiOutputNode.outputs[0].id,
edgeCaseNode.id,
edgeCaseNode.inputs[0].id,
[]
);
expect(edgeCaseNode.inputs[0].value).toBeNull();
expect(wf.edges.length).eq(0);
});
});

0 comments on commit 62e0c29

Please sign in to comment.