From 10e2699d296b5d26b5455291dd0ee0d9ebc49b10 Mon Sep 17 00:00:00 2001 From: guanghechen Date: Thu, 16 Mar 2023 16:17:08 +0800 Subject: [PATCH 1/8] feat: add findMapping --- package.json | 1 + packages/react-dag-editor/package.json | 1 + .../lib/utils/graphDiff/core/findMapping.ts | 54 +++++++++++++++++++ .../graphDiff/core/types/bipartite-graph.ts | 30 +++++++++++ .../lib/utils/graphDiff/core/types/index.ts | 1 + yarn.lock | 12 +++++ 6 files changed, 99 insertions(+) create mode 100644 packages/react-dag-editor/src/lib/utils/graphDiff/core/findMapping.ts create mode 100644 packages/react-dag-editor/src/lib/utils/graphDiff/core/types/bipartite-graph.ts create mode 100644 packages/react-dag-editor/src/lib/utils/graphDiff/core/types/index.ts diff --git a/package.json b/package.json index 739f95a8..3b0c44f4 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "private": true, "devDependencies": { + "@algorithm.ts/mcmf": "^2.0.14", "@babel/core": "7.12.13", "@babel/preset-typescript": "7.12.13", "@fluentui/merge-styles": "^8.2.0", diff --git a/packages/react-dag-editor/package.json b/packages/react-dag-editor/package.json index a8801241..5cb5f8d9 100644 --- a/packages/react-dag-editor/package.json +++ b/packages/react-dag-editor/package.json @@ -6,6 +6,7 @@ }, "version": "0.3.5", "dependencies": { + "@algorithm.ts/mcmf": "^2.0.14", "@fluentui/merge-styles": "^8.2.0", "eventemitter3": "^4.0.7", "react-jss": "~10.2.0", diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/findMapping.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/findMapping.ts new file mode 100644 index 00000000..6bff9c16 --- /dev/null +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/findMapping.ts @@ -0,0 +1,54 @@ +import { createMcmf } from "@algorithm.ts/mcmf"; +import { IBipartiteGraphNode, IMapping } from "./types"; + +const mcmf = createMcmf(); +export const findMapping = ( + candidates: IMapping[] +): IMapping[] => { + const sourceId = 0; + const sinkId = sourceId + 1; + mcmf.init(sourceId, sinkId, 2 + candidates.length * 2); + + let nextFlowNodeId = sinkId + 1; + const fromId2Mapping: Map> = new Map(); + + // Link flow source to the left graph node. + for (const mapping of candidates) { + const leftFlowNodeId = nextFlowNodeId++; + const rightFlowNodeId = nextFlowNodeId++; + + fromId2Mapping.set(leftFlowNodeId, mapping); + + // Link flow source to the left graph node. + mcmf.addEdge(sourceId, leftFlowNodeId, 1, 0); + + // Link left graph node to right graph node. + mcmf.addEdge(leftFlowNodeId, rightFlowNodeId, 1, mapping.cost.total); + + // Link right graph node to flow sink. + mcmf.addEdge(rightFlowNodeId, sinkId, 1, 0); + } + + // Run Min Cost Max Flow. + mcmf.minCostMaxFlow(); + + const filteredMappings: IMapping[] = []; + mcmf.solve(({ edges, edgeTot }) => { + for (let i = 0; i < edgeTot; i += 1) { + const edge = edges[i]; + + if ( + edge.cap > 0 && + edge.flow === edge.cap && + edge.from !== sourceId && + edge.to !== sinkId + ) { + const mapping = fromId2Mapping.get(edge.from); + if (mapping) { + filteredMappings.push(mapping); + } + } + } + }); + return filteredMappings; +}; diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/bipartite-graph.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/bipartite-graph.ts new file mode 100644 index 00000000..d6703814 --- /dev/null +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/bipartite-graph.ts @@ -0,0 +1,30 @@ +export enum BipartiteGraphType { + A = "A", + B = "B", +} + +export interface IBipartiteGraphNodeDiffCost { + /** + * Total diff cost of the two nodes linked by the edge. + * + * !!!Notice that the `total` may not equal with the `propertyDiff` + `structureDiff` since there + * may be other type of cost. + */ + total: number; + /** + * Diff cost of the properties of the two nodes linked by the edge. + */ + property: number; + /** + * Diff cost of the structure of the two nodes linked by the edge. + */ + structure: number; +} + +export interface IBipartiteGraphNode {} + +export interface IMapping { + leftNode: Node; + rightNode: Node; + cost: IBipartiteGraphNodeDiffCost; +} diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/index.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/index.ts new file mode 100644 index 00000000..fb745eb8 --- /dev/null +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/index.ts @@ -0,0 +1 @@ +export * from "./bipartite-graph"; diff --git a/yarn.lock b/yarn.lock index fe37494b..67ca9aee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,18 @@ # yarn lockfile v1 +"@algorithm.ts/circular-queue@^2.0.14": + version "2.0.14" + resolved "https://registry.yarnpkg.com/@algorithm.ts/circular-queue/-/circular-queue-2.0.14.tgz#1403a07cf800c8cdd461d97fcc9ff0d318ae2c29" + integrity sha512-lzwTHH7FNRLI1ze0S67pdc+QegetqvDTNdLVz28CaIuYM6wlFgdN1KVflayJ3O58nDriOL4Rgj0v5vNE6pRo1g== + +"@algorithm.ts/mcmf@^2.0.14": + version "2.0.14" + resolved "https://registry.yarnpkg.com/@algorithm.ts/mcmf/-/mcmf-2.0.14.tgz#b0e207f965efb6992b162561d9f0a85965a54bb0" + integrity sha512-mTJtZ9YAvcuILDs7VjH2Wh7iqZhbU6v0bXE/bkM5IdReDMlDwcbq1xyuHg/EmQ2OAjZU7Da3oPYOni3sRk7v6A== + dependencies: + "@algorithm.ts/circular-queue" "^2.0.14" + "@babel/code-frame@7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" From 7cce95c137c1688268e486f06d79d5c2e92d9848 Mon Sep 17 00:00:00 2001 From: guanghechen Date: Fri, 17 Mar 2023 10:12:12 +0800 Subject: [PATCH 2/8] feat: implement calculateStructureDiffCost --- .../graphDiff/core/types/bipartite-graph.ts | 30 ----- .../lib/utils/graphDiff/core/types/diff.ts | 48 ++++++++ .../lib/utils/graphDiff/core/types/graph.ts | 49 ++++++++ .../lib/utils/graphDiff/core/types/index.ts | 3 +- .../core/util/calculateStructureDiffCost.ts | 114 ++++++++++++++++++ .../graphDiff/core/{ => util}/findMapping.ts | 4 +- 6 files changed, 215 insertions(+), 33 deletions(-) delete mode 100644 packages/react-dag-editor/src/lib/utils/graphDiff/core/types/bipartite-graph.ts create mode 100644 packages/react-dag-editor/src/lib/utils/graphDiff/core/types/diff.ts create mode 100644 packages/react-dag-editor/src/lib/utils/graphDiff/core/types/graph.ts create mode 100644 packages/react-dag-editor/src/lib/utils/graphDiff/core/util/calculateStructureDiffCost.ts rename packages/react-dag-editor/src/lib/utils/graphDiff/core/{ => util}/findMapping.ts (92%) diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/bipartite-graph.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/bipartite-graph.ts deleted file mode 100644 index d6703814..00000000 --- a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/bipartite-graph.ts +++ /dev/null @@ -1,30 +0,0 @@ -export enum BipartiteGraphType { - A = "A", - B = "B", -} - -export interface IBipartiteGraphNodeDiffCost { - /** - * Total diff cost of the two nodes linked by the edge. - * - * !!!Notice that the `total` may not equal with the `propertyDiff` + `structureDiff` since there - * may be other type of cost. - */ - total: number; - /** - * Diff cost of the properties of the two nodes linked by the edge. - */ - property: number; - /** - * Diff cost of the structure of the two nodes linked by the edge. - */ - structure: number; -} - -export interface IBipartiteGraphNode {} - -export interface IMapping { - leftNode: Node; - rightNode: Node; - cost: IBipartiteGraphNodeDiffCost; -} diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/diff.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/diff.ts new file mode 100644 index 00000000..f6a538ef --- /dev/null +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/diff.ts @@ -0,0 +1,48 @@ +import { + IGraphEdge, + IGraphNode, + IGraphNodeDiffCost, + IGraphNodeWithStructure, +} from "./graph"; + +export interface IMapping { + lNode: Node; + rNode: Node; + cost: IGraphNodeDiffCost; +} + +export interface IGraphNodeDiffResult { + same: boolean; + /** + * Why are the two nodes considered different. + */ + reason?: string; + /** + * Additional debug data. + */ + details?: unknown; +} + +export interface IGraphDIffEnums { + StructureDiffCostRate: number; +} + +export interface IGraphDiffMethods< + Node extends IGraphNode, + Edge extends IGraphEdge +> { + areSameNodes( + lNode: Node, + rNode: Node, + lNodesMap: Map>, + rNodesMap: Map> + ): IGraphNodeDiffResult; +} + +export interface IGraphDiffContext< + Node extends IGraphNode, + Edge extends IGraphEdge +> { + enums: IGraphDIffEnums; + methods: IGraphDiffMethods; +} diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/graph.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/graph.ts new file mode 100644 index 00000000..1950e6b0 --- /dev/null +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/graph.ts @@ -0,0 +1,49 @@ +export enum GraphSource { + A = "A", + B = "B", +} + +export interface IGraphNode { + id: string; + hash: string | undefined; // For quickly comparing. +} + +export interface IGraphEdge {} + +export interface IGraphNodeOutgoingEdge< + Node extends IGraphNode, + Edge extends IGraphEdge +> { + portId: string; + targetNode: Node; + targetPortId: string; + originalEdge: Edge; +} + +export interface IGraphNodeWithStructure< + Node extends IGraphNode, + Edge extends IGraphEdge +> { + node: Node; + inEdges: IGraphNodeOutgoingEdge[]; + outEdges: IGraphNodeOutgoingEdge[]; +} + +export interface IGraphNodeDiffCost { + /** + * Total diff cost of the two nodes linked by the edge. + * + * !!!NOTICE + * There may be other type of cost, so the value of `.total` may not equal to + * the sum of `.property` and `.structure`. + */ + total: number; + /** + * Diff cost of the properties between the two nodes linked by the edge. + */ + property: number; + /** + * Diff cost of the structure between the two nodes linked by the edge. + */ + structure: number; +} diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/index.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/index.ts index fb745eb8..2254fc4a 100644 --- a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/index.ts +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/index.ts @@ -1 +1,2 @@ -export * from "./bipartite-graph"; +export * from "./diff"; +export * from "./graph"; diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/util/calculateStructureDiffCost.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/util/calculateStructureDiffCost.ts new file mode 100644 index 00000000..c8e1e41b --- /dev/null +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/util/calculateStructureDiffCost.ts @@ -0,0 +1,114 @@ +import { + IGraphDiffContext, + IGraphEdge, + IGraphNode, + IGraphNodeOutgoingEdge, + IGraphNodeWithStructure, +} from "../types"; + +export const calculateStructureDiffCost = < + Node extends IGraphNode, + Edge extends IGraphEdge +>( + lNode: IGraphNodeWithStructure, + rNode: IGraphNodeWithStructure, + lNodesMap: Map>, + rNodesMap: Map>, + context: IGraphDiffContext +): number => { + const { enums, methods } = context; + + const structureDiffCost = ( + lOutgoingEdges: IGraphNodeOutgoingEdge[], + rOutgoingEdges: IGraphNodeOutgoingEdge[] + ): number => { + const totalOutgoingEdges: number = + lOutgoingEdges.length + rOutgoingEdges.length; + if (totalOutgoingEdges === 0) { + return 0; + } + + const lPairedSet: Set = new Set(); + const rPairedSet: Set = new Set(); + + // Find exact same outgoing edge pairs. + for (let i = 0; i < lOutgoingEdges.length; i += 1) { + const loe = lOutgoingEdges[i]; + const j: number = rOutgoingEdges.findIndex( + (roe, idx) => + !rPairedSet.has(idx) && + loe.portId === roe.portId && + loe.targetPortId === roe.targetPortId && + loe.targetNode.hash && + loe.targetNode.hash === roe.targetNode.hash + ); + + if (j > -1) { + lPairedSet.add(i); + rPairedSet.add(j); + } + } + const countOfExactSame: number = lPairedSet.size; + + // Find strong similar outgoing edge pairs. + for (let i = 0; i < lOutgoingEdges.length; i += 1) { + const loe = lOutgoingEdges[i]; + if (!lPairedSet.has(i)) { + const j: number = rOutgoingEdges.findIndex( + (roe, idx) => + !rPairedSet.has(idx) && + methods.areSameNodes( + loe.targetNode, + roe.targetNode, + lNodesMap, + rNodesMap + ).same + ); + if (j > -1) { + lPairedSet.add(i); + rPairedSet.add(j); + } + } + } + const countOfStrongSimilar: number = lPairedSet.size - countOfExactSame; + + // Find weak similar node resource pairs. + for (let i = 0; i < lOutgoingEdges.length; i += 1) { + const loe = lOutgoingEdges[i]; + if (!lPairedSet.has(i)) { + const j: number = rOutgoingEdges.findIndex( + (roe, idx) => + !rPairedSet.has(idx) && + methods.areSameNodes( + loe.targetNode, + roe.targetNode, + lNodesMap, + rNodesMap + ).same + ); + if (j > -1) { + lPairedSet.add(i); + rPairedSet.add(j); + } + } + } + const countOfWeakSimilar: number = + lPairedSet.size - countOfExactSame - countOfStrongSimilar; + + const countOfNotSimilar: number = totalOutgoingEdges - lPairedSet.size; + const totalDiff: number = + countOfNotSimilar * 5 + countOfWeakSimilar * 2 + countOfStrongSimilar; + const cost: number = totalDiff * enums.StructureDiffCostRate; + return cost; + }; + + const ancestralDiffCost: number = structureDiffCost( + lNode.inEdges, + rNode.inEdges + ); + const descendantDiffCost: number = structureDiffCost( + lNode.outEdges, + rNode.outEdges + ); + return ancestralDiffCost + descendantDiffCost; +}; diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/findMapping.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/util/findMapping.ts similarity index 92% rename from packages/react-dag-editor/src/lib/utils/graphDiff/core/findMapping.ts rename to packages/react-dag-editor/src/lib/utils/graphDiff/core/util/findMapping.ts index 6bff9c16..c4d3e6db 100644 --- a/packages/react-dag-editor/src/lib/utils/graphDiff/core/findMapping.ts +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/util/findMapping.ts @@ -1,8 +1,8 @@ import { createMcmf } from "@algorithm.ts/mcmf"; -import { IBipartiteGraphNode, IMapping } from "./types"; +import { IGraphNode, IMapping } from "../types"; const mcmf = createMcmf(); -export const findMapping = ( +export const findMapping = ( candidates: IMapping[] ): IMapping[] => { const sourceId = 0; From bf35042bea7680c575d27561f943e50de6e8fe2d Mon Sep 17 00:00:00 2001 From: guanghechen Date: Fri, 17 Mar 2023 11:12:58 +0800 Subject: [PATCH 3/8] add BaseGraphDiffContext --- .vscode/settings.json | 41 +++++------ .../core/context/BaseGraphDiffContext.ts | 68 +++++++++++++++++++ .../context/defaultBuildCandidateMapping.ts | 50 ++++++++++++++ .../defaultCalcStructureDiffCost.ts} | 59 +++++++--------- .../core/types/{diff.ts => context.ts} | 31 ++++++--- .../lib/utils/graphDiff/core/types/graph.ts | 5 ++ .../lib/utils/graphDiff/core/types/index.ts | 2 +- 7 files changed, 191 insertions(+), 65 deletions(-) create mode 100644 packages/react-dag-editor/src/lib/utils/graphDiff/core/context/BaseGraphDiffContext.ts create mode 100644 packages/react-dag-editor/src/lib/utils/graphDiff/core/context/defaultBuildCandidateMapping.ts rename packages/react-dag-editor/src/lib/utils/graphDiff/core/{util/calculateStructureDiffCost.ts => context/defaultCalcStructureDiffCost.ts} (74%) rename packages/react-dag-editor/src/lib/utils/graphDiff/core/types/{diff.ts => context.ts} (53%) diff --git a/.vscode/settings.json b/.vscode/settings.json index be7dc6d7..0a81bd25 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,7 +6,7 @@ It represents the closest reasonable ESLint configuration to this project's original TSLint configuration. We recommend eventually switching this configuration to extend from -the recommended rulesets in typescript-eslint. +the recommended rulesets in typescript-eslint. https://github.com/typescript-eslint/tslint-to-eslint-config/blob/master/docs/FAQs.md Happy linting! 💖 @@ -29,26 +29,8 @@ Happy linting! 💖 }, "cSpell.ignoreWords": ["editor", "format", "on", "save"], "cSpell.words": [ - "Algos", - "Conda", - "Customizer", - "Databricks", - "Dataset", - "Datasets", - "Datastore", - "Datastores", - "Dismissable", - "Dropdown", - "Edat", - "Ensembling", - "Interop", - "Prefetcher", - "Resizable", - "SKUs", - "Serializers", - "Subsampling", - "Timespan", "abortcontroller", + "Algos", "appinsights", "automl", "azureml", @@ -58,12 +40,23 @@ Happy linting! 💖 "buildscripts", "callout", "cobertura", + "Conda", "continuationtoken", "cudatoolkit", + "Customizer", "cyclomatic", + "Databricks", "dataprep", + "Dataset", + "Datasets", + "Datastore", + "Datastores", "dcid", + "Dismissable", + "Dropdown", "eastus", + "Edat", + "Ensembling", "esnext", "etag", "experimentrun", @@ -75,6 +68,7 @@ Happy linting! 💖 "generageresult", "generatebuildresult", "hyperdrive", + "Interop", "jsnext", "jszip", "junit", @@ -84,6 +78,7 @@ Happy linting! 💖 "locstrings", "machinelearningservices", "managedenv", + "mcmf", "mlworkspace", "mockdate", "msal", @@ -96,8 +91,10 @@ Happy linting! 💖 "papaparse", "plotly", "polyfill", + "Prefetcher", "pytorch", "quickprofile", + "Resizable", "resjson", "rollup", "runhistory", @@ -105,8 +102,10 @@ Happy linting! 💖 "scriptrun", "scrollable", "serializer", + "Serializers", "setuptools", "sklearn", + "SKUs", "sourcemap", "spacy", "storyshots", @@ -114,10 +113,12 @@ Happy linting! 💖 "stylelint", "stylelintrc", "submodule", + "Subsampling", "svgr", "taskkill", "theming", "timeseries", + "Timespan", "treeshake", "tslib", "uifabric", diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/context/BaseGraphDiffContext.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/context/BaseGraphDiffContext.ts new file mode 100644 index 00000000..5c745392 --- /dev/null +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/context/BaseGraphDiffContext.ts @@ -0,0 +1,68 @@ +import { + GraphSource, + IGraph, + IGraphDiffContext, + IGraphEdge, + IGraphNode, + IGraphNodeDiffResult, + IGraphNodeWithStructure, + IMapping, +} from "../types"; +import { defaultBuildCandidateMapping } from "./defaultBuildCandidateMapping"; +import { defaultCalcStructureDiffCost } from "./defaultCalcStructureDiffCost"; + +export interface IBaseGraphDiffContextProps< + Node extends IGraphNode, + Edge extends IGraphEdge +> { + lNodesMap: ReadonlyMap>; + rNodesMap: ReadonlyMap>; +} + +export abstract class BaseGraphDiffContext< + Node extends IGraphNode, + Edge extends IGraphEdge +> implements IGraphDiffContext +{ + public readonly ABOnlyCostThreshold: number = 100000; + public readonly PropertyDiffCostRate: number = 0.2; + public readonly StructureDiffCostRate: number = 1; + + protected readonly lNodesMap: ReadonlyMap< + string, + IGraphNodeWithStructure + >; + protected readonly rNodesMap: ReadonlyMap< + string, + IGraphNodeWithStructure + >; + + public constructor(props: IBaseGraphDiffContextProps) { + this.lNodesMap = props.lNodesMap; + this.rNodesMap = props.rNodesMap; + } + + public abstract areSameNodes(lNode: Node, rNode: Node): IGraphNodeDiffResult; + + public buildCandidateMapping( + lGraph: IGraph, + rGraph: IGraph + ): IMapping[] { + return defaultBuildCandidateMapping(lGraph, rGraph, this); + } + + public calcStructureDiffCost(lNode: Node, rNode: Node): number { + return defaultCalcStructureDiffCost(lNode, rNode, this); + } + + public abstract calcPropertyDiffCost(lNode: Node, rNode: Node): number; + + public getGraphNodeWithStructure( + nodeId: string, + fromGraph: GraphSource + ): IGraphNodeWithStructure | undefined { + return fromGraph === GraphSource.A + ? this.lNodesMap.get(nodeId) + : this.rNodesMap.get(nodeId); + } +} diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/context/defaultBuildCandidateMapping.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/context/defaultBuildCandidateMapping.ts new file mode 100644 index 00000000..b86b58b7 --- /dev/null +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/context/defaultBuildCandidateMapping.ts @@ -0,0 +1,50 @@ +import { + IGraph, + IGraphDiffContext, + IGraphEdge, + IGraphNode, + IMapping, +} from "../types"; + +export const defaultBuildCandidateMapping = < + Node extends IGraphNode, + Edge extends IGraphEdge +>( + lGraph: IGraph, + rGraph: IGraph, + context: IGraphDiffContext +): IMapping[] => { + const { + ABOnlyCostThreshold, + areSameNodes, + calcStructureDiffCost, + calcPropertyDiffCost, + } = context; + + const lNodes: Node[] = lGraph.nodes; + const rNodes: Node[] = rGraph.nodes; + const candidates: IMapping[] = []; + for (let i = 0; i < lNodes.length; ++i) { + const lNode = lNodes[i]; + for (let j = 0; j < rNodes.length; ++j) { + const rNode = rNodes[j]; + if (rNode && areSameNodes(lNode, rNode).same) { + const structureDiffCost: number = calcStructureDiffCost(lNode, rNode); + const propertyDiffCost: number = calcPropertyDiffCost(lNode, rNode); + const diffCost: number = structureDiffCost + propertyDiffCost; + if (diffCost < ABOnlyCostThreshold) { + candidates.push({ + lNode, + rNode, + cost: { + total: diffCost, + property: propertyDiffCost, + structure: structureDiffCost, + }, + }); + } + } + } + } + return candidates; +}; diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/util/calculateStructureDiffCost.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/context/defaultCalcStructureDiffCost.ts similarity index 74% rename from packages/react-dag-editor/src/lib/utils/graphDiff/core/util/calculateStructureDiffCost.ts rename to packages/react-dag-editor/src/lib/utils/graphDiff/core/context/defaultCalcStructureDiffCost.ts index c8e1e41b..0ac6d9d6 100644 --- a/packages/react-dag-editor/src/lib/utils/graphDiff/core/util/calculateStructureDiffCost.ts +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/context/defaultCalcStructureDiffCost.ts @@ -1,27 +1,38 @@ import { + GraphSource, IGraphDiffContext, IGraphEdge, IGraphNode, IGraphNodeOutgoingEdge, - IGraphNodeWithStructure, } from "../types"; -export const calculateStructureDiffCost = < +export const defaultCalcStructureDiffCost = < Node extends IGraphNode, Edge extends IGraphEdge >( - lNode: IGraphNodeWithStructure, - rNode: IGraphNodeWithStructure, - lNodesMap: Map>, - rNodesMap: Map>, + lNode: Node, + rNode: Node, context: IGraphDiffContext ): number => { - const { enums, methods } = context; + const { StructureDiffCostRate, areSameNodes, getGraphNodeWithStructure } = + context; + const lNodeWithStructure = getGraphNodeWithStructure(lNode.id, GraphSource.A); + const rNodeWithStructure = getGraphNodeWithStructure(rNode.id, GraphSource.B); - const structureDiffCost = ( + const ancestralDiffCost: number = structureDiffCost( + lNodeWithStructure?.inEdges ?? [], + rNodeWithStructure?.inEdges ?? [] + ); + const descendantDiffCost: number = structureDiffCost( + lNodeWithStructure?.outEdges ?? [], + rNodeWithStructure?.outEdges ?? [] + ); + return ancestralDiffCost + descendantDiffCost; + + function structureDiffCost( lOutgoingEdges: IGraphNodeOutgoingEdge[], rOutgoingEdges: IGraphNodeOutgoingEdge[] - ): number => { + ): number { const totalOutgoingEdges: number = lOutgoingEdges.length + rOutgoingEdges.length; if (totalOutgoingEdges === 0) { @@ -57,12 +68,7 @@ export const calculateStructureDiffCost = < const j: number = rOutgoingEdges.findIndex( (roe, idx) => !rPairedSet.has(idx) && - methods.areSameNodes( - loe.targetNode, - roe.targetNode, - lNodesMap, - rNodesMap - ).same + areSameNodes(loe.targetNode, roe.targetNode).same ); if (j > -1) { lPairedSet.add(i); @@ -79,12 +85,7 @@ export const calculateStructureDiffCost = < const j: number = rOutgoingEdges.findIndex( (roe, idx) => !rPairedSet.has(idx) && - methods.areSameNodes( - loe.targetNode, - roe.targetNode, - lNodesMap, - rNodesMap - ).same + areSameNodes(loe.targetNode, roe.targetNode).same ); if (j > -1) { lPairedSet.add(i); @@ -95,20 +96,10 @@ export const calculateStructureDiffCost = < const countOfWeakSimilar: number = lPairedSet.size - countOfExactSame - countOfStrongSimilar; - const countOfNotSimilar: number = totalOutgoingEdges - lPairedSet.size; + const countOfNotSimilar: number = totalOutgoingEdges - lPairedSet.size * 2; const totalDiff: number = countOfNotSimilar * 5 + countOfWeakSimilar * 2 + countOfStrongSimilar; - const cost: number = totalDiff * enums.StructureDiffCostRate; + const cost: number = totalDiff * StructureDiffCostRate; return cost; - }; - - const ancestralDiffCost: number = structureDiffCost( - lNode.inEdges, - rNode.inEdges - ); - const descendantDiffCost: number = structureDiffCost( - lNode.outEdges, - rNode.outEdges - ); - return ancestralDiffCost + descendantDiffCost; + } }; diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/diff.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/context.ts similarity index 53% rename from packages/react-dag-editor/src/lib/utils/graphDiff/core/types/diff.ts rename to packages/react-dag-editor/src/lib/utils/graphDiff/core/types/context.ts index f6a538ef..6330a1bc 100644 --- a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/diff.ts +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/context.ts @@ -1,4 +1,6 @@ import { + GraphSource, + IGraph, IGraphEdge, IGraphNode, IGraphNodeDiffCost, @@ -24,6 +26,8 @@ export interface IGraphNodeDiffResult { } export interface IGraphDIffEnums { + ABOnlyCostThreshold: number; + PropertyDiffCostRate: number; StructureDiffCostRate: number; } @@ -31,18 +35,25 @@ export interface IGraphDiffMethods< Node extends IGraphNode, Edge extends IGraphEdge > { - areSameNodes( - lNode: Node, - rNode: Node, - lNodesMap: Map>, - rNodesMap: Map> - ): IGraphNodeDiffResult; + areSameNodes(lNode: Node, rNode: Node): IGraphNodeDiffResult; + + buildCandidateMapping( + lGraph: IGraph, + rGraph: IGraph + ): IMapping[]; + + calcStructureDiffCost(lNode: Node, rNode: Node): number; + + calcPropertyDiffCost(lNode: Node, rNode: Node): number; + + getGraphNodeWithStructure( + nodeId: string, + fromGraph: GraphSource + ): IGraphNodeWithStructure | undefined; } export interface IGraphDiffContext< Node extends IGraphNode, Edge extends IGraphEdge -> { - enums: IGraphDIffEnums; - methods: IGraphDiffMethods; -} +> extends IGraphDIffEnums, + IGraphDiffMethods {} diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/graph.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/graph.ts index 1950e6b0..8afe3352 100644 --- a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/graph.ts +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/graph.ts @@ -10,6 +10,11 @@ export interface IGraphNode { export interface IGraphEdge {} +export interface IGraph { + nodes: Node[]; + edges: Edge[]; +} + export interface IGraphNodeOutgoingEdge< Node extends IGraphNode, Edge extends IGraphEdge diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/index.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/index.ts index 2254fc4a..d15d7c56 100644 --- a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/index.ts +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/index.ts @@ -1,2 +1,2 @@ -export * from "./diff"; +export * from "./context"; export * from "./graph"; From fbfc2fdecec2b06af96bfcf73ccb24f499bda4ac Mon Sep 17 00:00:00 2001 From: guanghechen Date: Tue, 21 Mar 2023 09:52:39 +0800 Subject: [PATCH 4/8] :truck: rename findMapping to findBipartiteGraphMaxMapping --- ...{findMapping.ts => findBipartiteGraphMaxMapping.ts} | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) rename packages/react-dag-editor/src/lib/utils/graphDiff/core/util/{findMapping.ts => findBipartiteGraphMaxMapping.ts} (88%) diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/util/findMapping.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/util/findBipartiteGraphMaxMapping.ts similarity index 88% rename from packages/react-dag-editor/src/lib/utils/graphDiff/core/util/findMapping.ts rename to packages/react-dag-editor/src/lib/utils/graphDiff/core/util/findBipartiteGraphMaxMapping.ts index c4d3e6db..8268b4a5 100644 --- a/packages/react-dag-editor/src/lib/utils/graphDiff/core/util/findMapping.ts +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/util/findBipartiteGraphMaxMapping.ts @@ -2,7 +2,13 @@ import { createMcmf } from "@algorithm.ts/mcmf"; import { IGraphNode, IMapping } from "../types"; const mcmf = createMcmf(); -export const findMapping = ( + +/** + * Find the maximum matching with minimum cost in bipartite graph. + * + * @returns + */ +export const findBipartiteGraphMaxMapping = ( candidates: IMapping[] ): IMapping[] => { const sourceId = 0; @@ -50,5 +56,7 @@ export const findMapping = ( } } }); + + fromId2Mapping.clear(); return filteredMappings; }; From 5cce7d49b823a8477625efe52fec51ab116a2461 Mon Sep 17 00:00:00 2001 From: guanghechen Date: Tue, 21 Mar 2023 12:47:10 +0800 Subject: [PATCH 5/8] feat: add BaseGraphDiffResolver --- .../core/context/BaseGraphDiffContext.ts | 68 ----------- .../core/context/GraphDiffContext.ts | 101 +++++++++++++++++ .../core/resolver/BaseGraphDiffResolver.ts | 52 +++++++++ .../defaultBuildCandidateMapping.ts | 30 ++--- .../core/resolver/defaultBuildDiffNode.ts | 107 ++++++++++++++++++ .../defaultCalcStructureDiffCost.ts | 17 ++- .../lib/utils/graphDiff/core/types/context.ts | 48 +------- .../utils/graphDiff/core/types/diffGraph.ts | 31 +++++ .../lib/utils/graphDiff/core/types/graph.ts | 14 ++- .../lib/utils/graphDiff/core/types/index.ts | 2 + .../utils/graphDiff/core/types/resolver.ts | 64 +++++++++++ .../src/lib/utils/graphDiff/core/util/id.ts | 8 ++ 12 files changed, 408 insertions(+), 134 deletions(-) delete mode 100644 packages/react-dag-editor/src/lib/utils/graphDiff/core/context/BaseGraphDiffContext.ts create mode 100644 packages/react-dag-editor/src/lib/utils/graphDiff/core/context/GraphDiffContext.ts create mode 100644 packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/BaseGraphDiffResolver.ts rename packages/react-dag-editor/src/lib/utils/graphDiff/core/{context => resolver}/defaultBuildCandidateMapping.ts (63%) create mode 100644 packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultBuildDiffNode.ts rename packages/react-dag-editor/src/lib/utils/graphDiff/core/{context => resolver}/defaultCalcStructureDiffCost.ts (84%) create mode 100644 packages/react-dag-editor/src/lib/utils/graphDiff/core/types/diffGraph.ts create mode 100644 packages/react-dag-editor/src/lib/utils/graphDiff/core/types/resolver.ts create mode 100644 packages/react-dag-editor/src/lib/utils/graphDiff/core/util/id.ts diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/context/BaseGraphDiffContext.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/context/BaseGraphDiffContext.ts deleted file mode 100644 index 5c745392..00000000 --- a/packages/react-dag-editor/src/lib/utils/graphDiff/core/context/BaseGraphDiffContext.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { - GraphSource, - IGraph, - IGraphDiffContext, - IGraphEdge, - IGraphNode, - IGraphNodeDiffResult, - IGraphNodeWithStructure, - IMapping, -} from "../types"; -import { defaultBuildCandidateMapping } from "./defaultBuildCandidateMapping"; -import { defaultCalcStructureDiffCost } from "./defaultCalcStructureDiffCost"; - -export interface IBaseGraphDiffContextProps< - Node extends IGraphNode, - Edge extends IGraphEdge -> { - lNodesMap: ReadonlyMap>; - rNodesMap: ReadonlyMap>; -} - -export abstract class BaseGraphDiffContext< - Node extends IGraphNode, - Edge extends IGraphEdge -> implements IGraphDiffContext -{ - public readonly ABOnlyCostThreshold: number = 100000; - public readonly PropertyDiffCostRate: number = 0.2; - public readonly StructureDiffCostRate: number = 1; - - protected readonly lNodesMap: ReadonlyMap< - string, - IGraphNodeWithStructure - >; - protected readonly rNodesMap: ReadonlyMap< - string, - IGraphNodeWithStructure - >; - - public constructor(props: IBaseGraphDiffContextProps) { - this.lNodesMap = props.lNodesMap; - this.rNodesMap = props.rNodesMap; - } - - public abstract areSameNodes(lNode: Node, rNode: Node): IGraphNodeDiffResult; - - public buildCandidateMapping( - lGraph: IGraph, - rGraph: IGraph - ): IMapping[] { - return defaultBuildCandidateMapping(lGraph, rGraph, this); - } - - public calcStructureDiffCost(lNode: Node, rNode: Node): number { - return defaultCalcStructureDiffCost(lNode, rNode, this); - } - - public abstract calcPropertyDiffCost(lNode: Node, rNode: Node): number; - - public getGraphNodeWithStructure( - nodeId: string, - fromGraph: GraphSource - ): IGraphNodeWithStructure | undefined { - return fromGraph === GraphSource.A - ? this.lNodesMap.get(nodeId) - : this.rNodesMap.get(nodeId); - } -} diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/context/GraphDiffContext.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/context/GraphDiffContext.ts new file mode 100644 index 00000000..81294e6e --- /dev/null +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/context/GraphDiffContext.ts @@ -0,0 +1,101 @@ +import { + IGraph, + IGraphDiffContext, + IGraphEdge, + IGraphNode, + IGraphNodeOutgoingEdge, + IGraphNodeWithStructure, +} from "../types"; + +export interface IGraphDiffContextProps< + Node extends IGraphNode, + Edge extends IGraphEdge +> { + readonly lGraph: IGraph; + readonly rGraph: IGraph; +} + +export class GraphDiffContext + implements IGraphDiffContext +{ + public readonly ABOnlyCostThreshold: number = 100000; + public readonly PropertyDiffCostRate: number = 0.2; + public readonly StructureDiffCostRate: number = 1; + + public readonly lGraph: IGraph; + public readonly rGraph: IGraph; + + public readonly lNodesMap: ReadonlyMap< + string, + IGraphNodeWithStructure + >; + public readonly rNodesMap: ReadonlyMap< + string, + IGraphNodeWithStructure + >; + + public constructor(props: IGraphDiffContextProps) { + this.lGraph = props.lGraph; + this.rGraph = props.rGraph; + this.lNodesMap = this.buildNodeMap(props.lGraph); + this.rNodesMap = this.buildNodeMap(props.rGraph); + } + + protected buildNodeMap( + graph: IGraph + ): ReadonlyMap> { + const nodeMap: Map> = new Map(); + const idToNodeMap = new Map(); + for (const node of graph.nodes) { + idToNodeMap.set(node.id, node); + } + + for (const edge of graph.edges) { + const sourceNode = idToNodeMap.get(edge.source); + const targetNode = idToNodeMap.get(edge.target); + if (sourceNode && targetNode) { + { + const outEdge: IGraphNodeOutgoingEdge = { + portId: edge.sourcePort, + targetNode, + targetPortId: edge.targetPort, + originalEdge: edge, + }; + + const sourceNodeWithStructure = nodeMap.get(edge.source); + if (sourceNodeWithStructure) { + sourceNodeWithStructure.outEdges.push(outEdge); + } else { + nodeMap.set(edge.source, { + node: sourceNode, + inEdges: [], + outEdges: [outEdge], + }); + } + } + + { + const inEdge: IGraphNodeOutgoingEdge = { + portId: edge.targetPort, + targetNode: sourceNode, + targetPortId: edge.sourcePort, + originalEdge: edge, + }; + + const targetNodeWithStructure = nodeMap.get(edge.target); + if (targetNodeWithStructure) { + targetNodeWithStructure.inEdges.push(inEdge); + } else { + nodeMap.set(edge.target, { + node: targetNode, + inEdges: [inEdge], + outEdges: [], + }); + } + } + } + } + + return nodeMap; + } +} diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/BaseGraphDiffResolver.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/BaseGraphDiffResolver.ts new file mode 100644 index 00000000..c3900c59 --- /dev/null +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/BaseGraphDiffResolver.ts @@ -0,0 +1,52 @@ +import { + IGraphDiffContext, + IGraphDiffResolver, + IGraphEdge, + IGraphNode, + IGraphNodeDiffResult, + IMapping, +} from "../types"; +import { defaultBuildCandidateMapping } from "./defaultBuildCandidateMapping"; +import { defaultCalcStructureDiffCost } from "./defaultCalcStructureDiffCost"; + +export abstract class BaseGraphDiffResolver< + Node extends IGraphNode, + Edge extends IGraphEdge +> implements IGraphDiffResolver +{ + public abstract areSameNodes(lNode: Node, rNode: Node): IGraphNodeDiffResult; + + public buildCandidateMapping( + context: IGraphDiffContext + ): IMapping[] { + return defaultBuildCandidateMapping(context, this); + } + + public calcStructureDiffCost( + lNode: Node, + rNode: Node, + context: IGraphDiffContext + ): number { + return defaultCalcStructureDiffCost( + lNode, + rNode, + context, + this + ); + } + + public abstract calcPropertyDiffCost( + lNode: Node, + rNode: Node, + context: IGraphDiffContext + ): number; + + public hasSamePorts(lNode: Node, rNode: Node): boolean { + if (lNode.ports.length !== rNode.ports.length) { + return false; + } + + const lPortSet: Set = new Set(lNode.ports); + return rNode.ports.every((port) => lPortSet.has(port)); + } +} diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/context/defaultBuildCandidateMapping.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultBuildCandidateMapping.ts similarity index 63% rename from packages/react-dag-editor/src/lib/utils/graphDiff/core/context/defaultBuildCandidateMapping.ts rename to packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultBuildCandidateMapping.ts index b86b58b7..176780fd 100644 --- a/packages/react-dag-editor/src/lib/utils/graphDiff/core/context/defaultBuildCandidateMapping.ts +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultBuildCandidateMapping.ts @@ -1,6 +1,6 @@ import { - IGraph, IGraphDiffContext, + IGraphDiffResolver, IGraphEdge, IGraphNode, IMapping, @@ -10,27 +10,29 @@ export const defaultBuildCandidateMapping = < Node extends IGraphNode, Edge extends IGraphEdge >( - lGraph: IGraph, - rGraph: IGraph, - context: IGraphDiffContext + context: IGraphDiffContext, + resolver: IGraphDiffResolver ): IMapping[] => { - const { - ABOnlyCostThreshold, - areSameNodes, - calcStructureDiffCost, - calcPropertyDiffCost, - } = context; - + const { ABOnlyCostThreshold, lGraph, rGraph } = context; const lNodes: Node[] = lGraph.nodes; const rNodes: Node[] = rGraph.nodes; const candidates: IMapping[] = []; + for (let i = 0; i < lNodes.length; ++i) { const lNode = lNodes[i]; for (let j = 0; j < rNodes.length; ++j) { const rNode = rNodes[j]; - if (rNode && areSameNodes(lNode, rNode).same) { - const structureDiffCost: number = calcStructureDiffCost(lNode, rNode); - const propertyDiffCost: number = calcPropertyDiffCost(lNode, rNode); + if (rNode && resolver.areSameNodes(lNode, rNode).same) { + const structureDiffCost: number = resolver.calcStructureDiffCost( + lNode, + rNode, + context + ); + const propertyDiffCost: number = resolver.calcPropertyDiffCost( + lNode, + rNode, + context + ); const diffCost: number = structureDiffCost + propertyDiffCost; if (diffCost < ABOnlyCostThreshold) { candidates.push({ diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultBuildDiffNode.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultBuildDiffNode.ts new file mode 100644 index 00000000..94a3268d --- /dev/null +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultBuildDiffNode.ts @@ -0,0 +1,107 @@ +import { + GraphSource, + IABOnlyNode, + IGraphDiffResolver, + IGraphEdge, + IGraphNode, + IMappingWithDiffInfo, +} from "../types"; +import { GraphNodeDiffType, IDiffGraphNode } from "../types/diffGraph"; +import { genAutoIncrementId } from "../util/id"; + +export const defaultBuildDiffNodes = async < + Node extends IGraphNode, + Edge extends IGraphEdge +>( + mappings: IMappingWithDiffInfo[], + abOnlyNodes: IABOnlyNode[], + resolver: IGraphDiffResolver +): Promise<{ + diffGraphNodes: IDiffGraphNode[]; + diffNodeMap: Map>; + lNodeId2diffNodeMap: Map>; + rNodeId2diffNodeMap: Map>; +}> => { + const genDiffGraphId = genAutoIncrementId(); + const diffGraphNodes: IDiffGraphNode[] = []; + const diffNodeMap = new Map>(); + const lNodeId2diffNodeMap = new Map>(); + const rNodeId2diffNodeMap = new Map>(); + + const addDiffNode = (diffNode: IDiffGraphNode): void => { + diffGraphNodes.push(diffNode); + diffNodeMap.set(diffNode.id, diffNode); + if (diffNode.lNode) { + lNodeId2diffNodeMap.set(diffNode.lNode.id, diffNode); + } + if (diffNode.rNode) { + rNodeId2diffNodeMap.set(diffNode.rNode.id, diffNode); + } + }; + + const addAOnlyNode = (node: Node): void => { + const diffNode: IDiffGraphNode = { + id: genDiffGraphId.next().value, + diffType: GraphNodeDiffType.AOnly, + lNode: node, + rNode: undefined, + }; + addDiffNode(diffNode); + }; + + const addBOnlyNode = (node: Node): void => { + const diffNode: IDiffGraphNode = { + id: genDiffGraphId.next().value, + diffType: GraphNodeDiffType.BOnly, + lNode: undefined, + rNode: node, + }; + addDiffNode(diffNode); + }; + + // A only / B only nodes. + abOnlyNodes.forEach((item) => { + if (item.active) { + switch (item.fromGraph) { + case GraphSource.A: + addAOnlyNode(item.node); + break; + case GraphSource.B: + addBOnlyNode(item.node); + break; + default: + } + } + }); + + // Paired nodes. + for (const mapping of mappings) { + const { lNode, rNode } = mapping; + const diffNode: IDiffGraphNode = { + id: genDiffGraphId.next().value, + diffType: GraphNodeDiffType.equal, + lNode, + rNode, + }; + + if (!resolver.hasSamePorts(lNode, rNode)) { + diffNode.diffType = GraphNodeDiffType.portChanged; + addDiffNode(diffNode); + continue; + } + + if (mapping.cost.property !== 0) { + diffNode.diffType = GraphNodeDiffType.propertyChanged; + addDiffNode(diffNode); + continue; + } + + addDiffNode(diffNode); + } + return { + diffGraphNodes, + diffNodeMap, + lNodeId2diffNodeMap, + rNodeId2diffNodeMap, + }; +}; diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/context/defaultCalcStructureDiffCost.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultCalcStructureDiffCost.ts similarity index 84% rename from packages/react-dag-editor/src/lib/utils/graphDiff/core/context/defaultCalcStructureDiffCost.ts rename to packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultCalcStructureDiffCost.ts index 0ac6d9d6..933bf648 100644 --- a/packages/react-dag-editor/src/lib/utils/graphDiff/core/context/defaultCalcStructureDiffCost.ts +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultCalcStructureDiffCost.ts @@ -1,6 +1,6 @@ import { - GraphSource, IGraphDiffContext, + IGraphDiffResolver, IGraphEdge, IGraphNode, IGraphNodeOutgoingEdge, @@ -12,12 +12,11 @@ export const defaultCalcStructureDiffCost = < >( lNode: Node, rNode: Node, - context: IGraphDiffContext + context: IGraphDiffContext, + resolver: IGraphDiffResolver ): number => { - const { StructureDiffCostRate, areSameNodes, getGraphNodeWithStructure } = - context; - const lNodeWithStructure = getGraphNodeWithStructure(lNode.id, GraphSource.A); - const rNodeWithStructure = getGraphNodeWithStructure(rNode.id, GraphSource.B); + const lNodeWithStructure = context.lNodesMap.get(lNode.id); + const rNodeWithStructure = context.rNodesMap.get(rNode.id); const ancestralDiffCost: number = structureDiffCost( lNodeWithStructure?.inEdges ?? [], @@ -68,7 +67,7 @@ export const defaultCalcStructureDiffCost = < const j: number = rOutgoingEdges.findIndex( (roe, idx) => !rPairedSet.has(idx) && - areSameNodes(loe.targetNode, roe.targetNode).same + resolver.areSameNodes(loe.targetNode, roe.targetNode).same ); if (j > -1) { lPairedSet.add(i); @@ -85,7 +84,7 @@ export const defaultCalcStructureDiffCost = < const j: number = rOutgoingEdges.findIndex( (roe, idx) => !rPairedSet.has(idx) && - areSameNodes(loe.targetNode, roe.targetNode).same + resolver.areSameNodes(loe.targetNode, roe.targetNode).same ); if (j > -1) { lPairedSet.add(i); @@ -99,7 +98,7 @@ export const defaultCalcStructureDiffCost = < const countOfNotSimilar: number = totalOutgoingEdges - lPairedSet.size * 2; const totalDiff: number = countOfNotSimilar * 5 + countOfWeakSimilar * 2 + countOfStrongSimilar; - const cost: number = totalDiff * StructureDiffCostRate; + const cost: number = totalDiff * context.StructureDiffCostRate; return cost; } }; diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/context.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/context.ts index 6330a1bc..99426a29 100644 --- a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/context.ts +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/context.ts @@ -1,59 +1,23 @@ import { - GraphSource, IGraph, IGraphEdge, IGraphNode, - IGraphNodeDiffCost, IGraphNodeWithStructure, } from "./graph"; -export interface IMapping { - lNode: Node; - rNode: Node; - cost: IGraphNodeDiffCost; -} - -export interface IGraphNodeDiffResult { - same: boolean; - /** - * Why are the two nodes considered different. - */ - reason?: string; - /** - * Additional debug data. - */ - details?: unknown; -} - export interface IGraphDIffEnums { ABOnlyCostThreshold: number; PropertyDiffCostRate: number; StructureDiffCostRate: number; } -export interface IGraphDiffMethods< +export interface IGraphDiffContext< Node extends IGraphNode, Edge extends IGraphEdge -> { - areSameNodes(lNode: Node, rNode: Node): IGraphNodeDiffResult; - - buildCandidateMapping( - lGraph: IGraph, - rGraph: IGraph - ): IMapping[]; +> extends IGraphDIffEnums { + readonly lGraph: IGraph; + readonly rGraph: IGraph; - calcStructureDiffCost(lNode: Node, rNode: Node): number; - - calcPropertyDiffCost(lNode: Node, rNode: Node): number; - - getGraphNodeWithStructure( - nodeId: string, - fromGraph: GraphSource - ): IGraphNodeWithStructure | undefined; + readonly lNodesMap: ReadonlyMap>; + readonly rNodesMap: ReadonlyMap>; } - -export interface IGraphDiffContext< - Node extends IGraphNode, - Edge extends IGraphEdge -> extends IGraphDIffEnums, - IGraphDiffMethods {} diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/diffGraph.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/diffGraph.ts new file mode 100644 index 00000000..1425f58e --- /dev/null +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/diffGraph.ts @@ -0,0 +1,31 @@ +import { IGraphEdge, IGraphNode } from "./graph"; + +export enum GraphNodeDiffType { + equal = "equal", + AOnly = "AOnly", // A only. + BOnly = "BOnly", // B only. + portChanged = "portChanged", + propertyChanged = "propertyChanged", +} + +export enum GraphEdgeDiffType { + equal = "equal", + AOnly = "AOnly ", // A only. + BOnly = "BOnly", // B only. +} + +export interface IDiffGraphNode { + id: number; // DiffGraph node id. + diffType: GraphNodeDiffType; + lNode: Node | undefined; // Node from GraphSource.A + rNode: Node | undefined; // Node from GraphSource.B +} + +export interface IDiffGraphEdge { + id: number; // DiffGraph edge id. + diffType: GraphEdgeDiffType; + lDiffNodeId: string; + rDiffNodeId: string; + lEdge: Edge | undefined; // Edge from GraphSource.A + rEdge: Edge | undefined; // Edge from GraphSource.B +} diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/graph.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/graph.ts index 8afe3352..0a10dd18 100644 --- a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/graph.ts +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/graph.ts @@ -5,10 +5,16 @@ export enum GraphSource { export interface IGraphNode { id: string; + ports: string[]; hash: string | undefined; // For quickly comparing. } -export interface IGraphEdge {} +export interface IGraphEdge { + source: string; + sourcePort: string; + target: string; + targetPort: string; +} export interface IGraph { nodes: Node[]; @@ -52,3 +58,9 @@ export interface IGraphNodeDiffCost { */ structure: number; } + +export interface IABOnlyNode { + fromGraph: GraphSource; + node: Node; + active: boolean; +} diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/index.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/index.ts index d15d7c56..c0dfff33 100644 --- a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/index.ts +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/index.ts @@ -1,2 +1,4 @@ export * from "./context"; +export * from "./diffGraph"; export * from "./graph"; +export * from "./resolver"; diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/resolver.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/resolver.ts new file mode 100644 index 00000000..ed424e65 --- /dev/null +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/resolver.ts @@ -0,0 +1,64 @@ +import { IGraphDiffContext } from "./context"; +import { + GraphSource, + IGraphEdge, + IGraphNode, + IGraphNodeDiffCost, +} from "./graph"; + +export interface IMapping { + lNode: Node; + rNode: Node; + cost: IGraphNodeDiffCost; +} + +export interface IMappingWithDiffInfo< + Node extends IGraphNode, + Edge extends IGraphEdge +> extends IMapping { + diffNodeId: string; + overrideEdges: Array<{ + fromGraph: GraphSource; + edge: Edge; + sourceDiffNodeId?: string; + targetDiffNodeId?: string; + }>; +} + +export interface IGraphNodeDiffResult { + same: boolean; + /** + * Why are the two nodes considered different. + */ + reason?: string; + /** + * Additional debug data. + */ + details?: unknown; +} + +export interface IGraphDiffResolver< + Node extends IGraphNode, + Edge extends IGraphEdge +> { + areSameNodes(lNode: Node, rNode: Node): IGraphNodeDiffResult; + + buildCandidateMapping( + context: IGraphDiffContext + ): IMapping[]; + + calcStructureDiffCost( + lNode: Node, + rNode: Node, + context: IGraphDiffContext + ): number; + + calcPropertyDiffCost( + lNode: Node, + rNode: Node, + context: IGraphDiffContext + ): number; + + // Check if two nodes have same ports. + hasSamePorts(lNode: Node, rNode: Node): boolean; +} diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/util/id.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/util/id.ts new file mode 100644 index 00000000..b5beffc6 --- /dev/null +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/util/id.ts @@ -0,0 +1,8 @@ +export function* genAutoIncrementId( + start = 0 +): Iterator { + let i: number = start; + for (; ; ++i) { + yield i; + } +} From 6d7735dda0e359209d13837033245e884179e349 Mon Sep 17 00:00:00 2001 From: guanghechen Date: Tue, 21 Mar 2023 18:06:42 +0800 Subject: [PATCH 6/8] add buildDiffGraph --- .../core/resolver/BaseGraphDiffResolver.ts | 34 +++++++ .../resolver/defaultBuildCandidateMapping.ts | 6 +- .../core/resolver/defaultBuildDiffEdge.ts | 92 +++++++++++++++++++ .../core/resolver/defaultBuildDiffGraph.ts | 27 ++++++ .../core/resolver/defaultBuildDiffNode.ts | 48 +++++----- .../resolver/defaultCalcStructureDiffCost.ts | 6 +- .../utils/graphDiff/core/types/diffGraph.ts | 30 +++++- .../lib/utils/graphDiff/core/types/graph.ts | 6 -- .../utils/graphDiff/core/types/resolver.ts | 47 ++++++---- .../util/{id.ts => genAutoIncrementId.ts} | 6 +- 10 files changed, 242 insertions(+), 60 deletions(-) create mode 100644 packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultBuildDiffEdge.ts create mode 100644 packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultBuildDiffGraph.ts rename packages/react-dag-editor/src/lib/utils/graphDiff/core/util/{id.ts => genAutoIncrementId.ts} (61%) diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/BaseGraphDiffResolver.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/BaseGraphDiffResolver.ts index c3900c59..d64387fe 100644 --- a/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/BaseGraphDiffResolver.ts +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/BaseGraphDiffResolver.ts @@ -1,4 +1,9 @@ import { + IABOnlyNode, + IDiffGraph, + IDiffGraphEdge, + IDiffGraphNode, + IDiffGraphNodeSearcher, IGraphDiffContext, IGraphDiffResolver, IGraphEdge, @@ -7,6 +12,9 @@ import { IMapping, } from "../types"; import { defaultBuildCandidateMapping } from "./defaultBuildCandidateMapping"; +import { defaultBuildDiffEdges } from "./defaultBuildDiffEdge"; +import { defaultBuildDiffNodes } from "./defaultBuildDiffNode"; +import { defaultBuildDiffGraph } from "./defaultBuildDiffGraph"; import { defaultCalcStructureDiffCost } from "./defaultCalcStructureDiffCost"; export abstract class BaseGraphDiffResolver< @@ -22,6 +30,32 @@ export abstract class BaseGraphDiffResolver< return defaultBuildCandidateMapping(context, this); } + public buildDiffEdges( + diffNodeSearcher: IDiffGraphNodeSearcher, + context: IGraphDiffContext + ): { diffEdges: IDiffGraphEdge[] } { + return defaultBuildDiffEdges(diffNodeSearcher, context); + } + + buildDiffGraph( + mappings: IMapping[], + abOnlyNodes: IABOnlyNode[], + context: IGraphDiffContext + ): IDiffGraph { + return defaultBuildDiffGraph(mappings, abOnlyNodes, context, this); + } + + public buildDiffNodes( + mappings: IMapping[], + abOnlyNodes: IABOnlyNode[], + _context: IGraphDiffContext + ): { + diffNodes: IDiffGraphNode[]; + diffNodeSearcher: IDiffGraphNodeSearcher; + } { + return defaultBuildDiffNodes(mappings, abOnlyNodes, this); + } + public calcStructureDiffCost( lNode: Node, rNode: Node, diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultBuildCandidateMapping.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultBuildCandidateMapping.ts index 176780fd..0742e5b2 100644 --- a/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultBuildCandidateMapping.ts +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultBuildCandidateMapping.ts @@ -6,13 +6,13 @@ import { IMapping, } from "../types"; -export const defaultBuildCandidateMapping = < +export function defaultBuildCandidateMapping< Node extends IGraphNode, Edge extends IGraphEdge >( context: IGraphDiffContext, resolver: IGraphDiffResolver -): IMapping[] => { +): IMapping[] { const { ABOnlyCostThreshold, lGraph, rGraph } = context; const lNodes: Node[] = lGraph.nodes; const rNodes: Node[] = rGraph.nodes; @@ -49,4 +49,4 @@ export const defaultBuildCandidateMapping = < } } return candidates; -}; +} diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultBuildDiffEdge.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultBuildDiffEdge.ts new file mode 100644 index 00000000..2e79d195 --- /dev/null +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultBuildDiffEdge.ts @@ -0,0 +1,92 @@ +import { IGraphDiffContext, IGraphEdge, IGraphNode } from "../types"; +import { + GraphEdgeDiffType, + IDiffGraphEdge, + IDiffGraphNode, + IDiffGraphNodeSearcher, +} from "../types/diffGraph"; + +export function defaultBuildDiffEdges< + Node extends IGraphNode, + Edge extends IGraphEdge +>( + diffNodeSearcher: IDiffGraphNodeSearcher, + context: IGraphDiffContext +): { + diffEdges: IDiffGraphEdge[]; +} { + const genDiffEdgeId = ( + sourceDiffNode: IDiffGraphNode, + targetDiffNode: IDiffGraphNode, + sourcePort: string, + targetPort: string + ): string => + `${sourceDiffNode.id}:${sourcePort}#${targetDiffNode.id}:${targetPort}`; + + const diffGraphEdgeMap: Map> = new Map(); + + for (const edge of context.lGraph.edges) { + const sourceDiffNode = diffNodeSearcher.lNodeId2diffNodeMap.get( + edge.source + ); + const targetDiffNode = diffNodeSearcher.rNodeId2diffNodeMap.get( + edge.target + ); + + if (sourceDiffNode && targetDiffNode) { + const diffEdgeKey: string = genDiffEdgeId( + sourceDiffNode, + targetDiffNode, + edge.sourcePort, + edge.targetPort + ); + + const diffEdge: IDiffGraphEdge = { + id: diffEdgeKey, + diffType: GraphEdgeDiffType.AOnly, + sourceDiffNode, + targetDiffNode, + lEdge: edge, + rEdge: undefined, + }; + diffGraphEdgeMap.set(diffEdgeKey, diffEdge); + } + } + + for (const edge of context.rGraph.edges) { + const sourceDiffNode = diffNodeSearcher.rNodeId2diffNodeMap.get( + edge.source + ); + const targetDiffNode = diffNodeSearcher.rNodeId2diffNodeMap.get( + edge.target + ); + + if (sourceDiffNode && targetDiffNode) { + const diffEdgeKey: string = genDiffEdgeId( + sourceDiffNode, + targetDiffNode, + edge.sourcePort, + edge.targetPort + ); + + const existedDiffEdge = diffGraphEdgeMap.get(diffEdgeKey); + if (existedDiffEdge) { + existedDiffEdge.diffType = GraphEdgeDiffType.equal; + existedDiffEdge.rEdge = edge; + } else { + const diffEdge: IDiffGraphEdge = { + id: diffEdgeKey, + diffType: GraphEdgeDiffType.BOnly, + sourceDiffNode, + targetDiffNode, + lEdge: undefined, + rEdge: edge, + }; + diffGraphEdgeMap.set(diffEdgeKey, diffEdge); + } + } + } + return { + diffEdges: Array.from(diffGraphEdgeMap.values()), + }; +} diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultBuildDiffGraph.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultBuildDiffGraph.ts new file mode 100644 index 00000000..0dc5688a --- /dev/null +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultBuildDiffGraph.ts @@ -0,0 +1,27 @@ +import { + IABOnlyNode, + IGraphDiffContext, + IGraphDiffResolver, + IGraphEdge, + IGraphNode, + IMapping, +} from "../types"; +import { IDiffGraph } from "../types/diffGraph"; + +export function defaultBuildDiffGraph< + Node extends IGraphNode, + Edge extends IGraphEdge +>( + mappings: IMapping[], + abOnlyNodes: IABOnlyNode[], + context: IGraphDiffContext, + resolver: IGraphDiffResolver +): IDiffGraph { + const { diffNodes, diffNodeSearcher } = resolver.buildDiffNodes( + mappings, + abOnlyNodes, + context + ); + const { diffEdges } = resolver.buildDiffEdges(diffNodeSearcher, context); + return { diffNodes, diffEdges }; +} diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultBuildDiffNode.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultBuildDiffNode.ts index 94a3268d..4c9b76ae 100644 --- a/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultBuildDiffNode.ts +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultBuildDiffNode.ts @@ -4,32 +4,34 @@ import { IGraphDiffResolver, IGraphEdge, IGraphNode, - IMappingWithDiffInfo, + IMapping, } from "../types"; -import { GraphNodeDiffType, IDiffGraphNode } from "../types/diffGraph"; -import { genAutoIncrementId } from "../util/id"; +import { + GraphNodeDiffType, + IDiffGraphNode, + IDiffGraphNodeSearcher, +} from "../types/diffGraph"; +import { genAutoIncrementId } from "../util/genAutoIncrementId"; -export const defaultBuildDiffNodes = async < +export function defaultBuildDiffNodes< Node extends IGraphNode, Edge extends IGraphEdge >( - mappings: IMappingWithDiffInfo[], + mappings: IMapping[], abOnlyNodes: IABOnlyNode[], resolver: IGraphDiffResolver -): Promise<{ - diffGraphNodes: IDiffGraphNode[]; - diffNodeMap: Map>; - lNodeId2diffNodeMap: Map>; - rNodeId2diffNodeMap: Map>; -}> => { - const genDiffGraphId = genAutoIncrementId(); - const diffGraphNodes: IDiffGraphNode[] = []; +): { + diffNodes: IDiffGraphNode[]; + diffNodeSearcher: IDiffGraphNodeSearcher; +} { + const genDiffNodeId = genAutoIncrementId(); + const diffNodes: IDiffGraphNode[] = []; const diffNodeMap = new Map>(); const lNodeId2diffNodeMap = new Map>(); const rNodeId2diffNodeMap = new Map>(); const addDiffNode = (diffNode: IDiffGraphNode): void => { - diffGraphNodes.push(diffNode); + diffNodes.push(diffNode); diffNodeMap.set(diffNode.id, diffNode); if (diffNode.lNode) { lNodeId2diffNodeMap.set(diffNode.lNode.id, diffNode); @@ -41,7 +43,7 @@ export const defaultBuildDiffNodes = async < const addAOnlyNode = (node: Node): void => { const diffNode: IDiffGraphNode = { - id: genDiffGraphId.next().value, + id: genDiffNodeId.next().value, diffType: GraphNodeDiffType.AOnly, lNode: node, rNode: undefined, @@ -51,7 +53,7 @@ export const defaultBuildDiffNodes = async < const addBOnlyNode = (node: Node): void => { const diffNode: IDiffGraphNode = { - id: genDiffGraphId.next().value, + id: genDiffNodeId.next().value, diffType: GraphNodeDiffType.BOnly, lNode: undefined, rNode: node, @@ -78,7 +80,7 @@ export const defaultBuildDiffNodes = async < for (const mapping of mappings) { const { lNode, rNode } = mapping; const diffNode: IDiffGraphNode = { - id: genDiffGraphId.next().value, + id: genDiffNodeId.next().value, diffType: GraphNodeDiffType.equal, lNode, rNode, @@ -99,9 +101,11 @@ export const defaultBuildDiffNodes = async < addDiffNode(diffNode); } return { - diffGraphNodes, - diffNodeMap, - lNodeId2diffNodeMap, - rNodeId2diffNodeMap, + diffNodes, + diffNodeSearcher: { + diffNodeMap, + lNodeId2diffNodeMap, + rNodeId2diffNodeMap, + }, }; -}; +} diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultCalcStructureDiffCost.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultCalcStructureDiffCost.ts index 933bf648..7160d512 100644 --- a/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultCalcStructureDiffCost.ts +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/resolver/defaultCalcStructureDiffCost.ts @@ -6,7 +6,7 @@ import { IGraphNodeOutgoingEdge, } from "../types"; -export const defaultCalcStructureDiffCost = < +export function defaultCalcStructureDiffCost< Node extends IGraphNode, Edge extends IGraphEdge >( @@ -14,7 +14,7 @@ export const defaultCalcStructureDiffCost = < rNode: Node, context: IGraphDiffContext, resolver: IGraphDiffResolver -): number => { +): number { const lNodeWithStructure = context.lNodesMap.get(lNode.id); const rNodeWithStructure = context.rNodesMap.get(rNode.id); @@ -101,4 +101,4 @@ export const defaultCalcStructureDiffCost = < const cost: number = totalDiff * context.StructureDiffCostRate; return cost; } -}; +} diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/diffGraph.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/diffGraph.ts index 1425f58e..85de44e1 100644 --- a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/diffGraph.ts +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/diffGraph.ts @@ -1,4 +1,4 @@ -import { IGraphEdge, IGraphNode } from "./graph"; +import { GraphSource, IGraphEdge, IGraphNode } from "./graph"; export enum GraphNodeDiffType { equal = "equal", @@ -21,11 +21,31 @@ export interface IDiffGraphNode { rNode: Node | undefined; // Node from GraphSource.B } -export interface IDiffGraphEdge { - id: number; // DiffGraph edge id. +export interface IDiffGraphEdge< + Node extends IGraphNode, + Edge extends IGraphEdge +> { + id: string; // DiffGraph edge id. diffType: GraphEdgeDiffType; - lDiffNodeId: string; - rDiffNodeId: string; + sourceDiffNode: IDiffGraphNode; + targetDiffNode: IDiffGraphNode; lEdge: Edge | undefined; // Edge from GraphSource.A rEdge: Edge | undefined; // Edge from GraphSource.B } + +export interface IDiffGraph { + diffNodes: IDiffGraphNode[]; + diffEdges: IDiffGraphEdge[]; +} + +export interface IDiffGraphNodeSearcher { + diffNodeMap: Map>; + lNodeId2diffNodeMap: Map>; + rNodeId2diffNodeMap: Map>; +} + +export interface IABOnlyNode { + fromGraph: GraphSource; + node: Node; + active: boolean; +} diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/graph.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/graph.ts index 0a10dd18..eb7f8fa0 100644 --- a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/graph.ts +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/graph.ts @@ -58,9 +58,3 @@ export interface IGraphNodeDiffCost { */ structure: number; } - -export interface IABOnlyNode { - fromGraph: GraphSource; - node: Node; - active: boolean; -} diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/resolver.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/resolver.ts index ed424e65..3982988c 100644 --- a/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/resolver.ts +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/types/resolver.ts @@ -1,10 +1,12 @@ import { IGraphDiffContext } from "./context"; import { - GraphSource, - IGraphEdge, - IGraphNode, - IGraphNodeDiffCost, -} from "./graph"; + IABOnlyNode, + IDiffGraph, + IDiffGraphEdge, + IDiffGraphNode, + IDiffGraphNodeSearcher, +} from "./diffGraph"; +import { IGraphEdge, IGraphNode, IGraphNodeDiffCost } from "./graph"; export interface IMapping { lNode: Node; @@ -12,19 +14,6 @@ export interface IMapping { cost: IGraphNodeDiffCost; } -export interface IMappingWithDiffInfo< - Node extends IGraphNode, - Edge extends IGraphEdge -> extends IMapping { - diffNodeId: string; - overrideEdges: Array<{ - fromGraph: GraphSource; - edge: Edge; - sourceDiffNodeId?: string; - targetDiffNodeId?: string; - }>; -} - export interface IGraphNodeDiffResult { same: boolean; /** @@ -47,6 +36,28 @@ export interface IGraphDiffResolver< context: IGraphDiffContext ): IMapping[]; + buildDiffEdges( + diffNodeSearcher: IDiffGraphNodeSearcher, + context: IGraphDiffContext + ): { + diffEdges: IDiffGraphEdge[]; + }; + + buildDiffGraph( + mappings: IMapping[], + abOnlyNodes: IABOnlyNode[], + context: IGraphDiffContext + ): IDiffGraph; + + buildDiffNodes( + mappings: IMapping[], + abOnlyNodes: IABOnlyNode[], + context: IGraphDiffContext + ): { + diffNodes: IDiffGraphNode[]; + diffNodeSearcher: IDiffGraphNodeSearcher; + }; + calcStructureDiffCost( lNode: Node, rNode: Node, diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/util/id.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/util/genAutoIncrementId.ts similarity index 61% rename from packages/react-dag-editor/src/lib/utils/graphDiff/core/util/id.ts rename to packages/react-dag-editor/src/lib/utils/graphDiff/core/util/genAutoIncrementId.ts index b5beffc6..22eadc8f 100644 --- a/packages/react-dag-editor/src/lib/utils/graphDiff/core/util/id.ts +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/util/genAutoIncrementId.ts @@ -1,8 +1,8 @@ export function* genAutoIncrementId( start = 0 ): Iterator { - let i: number = start; - for (; ; ++i) { - yield i; + let id: number = start; + for (; ; ++id) { + yield id; } } From 6589b4a462028d31517f5cd95b2055cbd3cd7f8a Mon Sep 17 00:00:00 2001 From: guanghechen Date: Tue, 21 Mar 2023 18:11:34 +0800 Subject: [PATCH 7/8] add entry --- .../src/lib/utils/graphDiff/core/index.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 packages/react-dag-editor/src/lib/utils/graphDiff/core/index.ts diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/index.ts b/packages/react-dag-editor/src/lib/utils/graphDiff/core/index.ts new file mode 100644 index 00000000..849812c3 --- /dev/null +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/index.ts @@ -0,0 +1,10 @@ +export * from "./context/GraphDiffContext"; +export * from "./resolver/BaseGraphDiffResolver"; +export * from "./resolver/defaultBuildCandidateMapping"; +export * from "./resolver/defaultBuildDiffEdge"; +export * from "./resolver/defaultBuildDiffGraph"; +export * from "./resolver/defaultBuildDiffNode"; +export * from "./resolver/defaultCalcStructureDiffCost"; +export * from "./util/findBipartiteGraphMaxMapping"; +export * from "./util/genAutoIncrementId"; +export * from "./types"; From c5f3caf1e464bf9e2e14f1f3789b49fdd9ede837 Mon Sep 17 00:00:00 2001 From: guanghechen Date: Mon, 3 Apr 2023 16:13:57 +0800 Subject: [PATCH 8/8] update README --- .../src/lib/utils/graphDiff/core/README.md | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 packages/react-dag-editor/src/lib/utils/graphDiff/core/README.md diff --git a/packages/react-dag-editor/src/lib/utils/graphDiff/core/README.md b/packages/react-dag-editor/src/lib/utils/graphDiff/core/README.md new file mode 100644 index 00000000..f41237c0 --- /dev/null +++ b/packages/react-dag-editor/src/lib/utils/graphDiff/core/README.md @@ -0,0 +1,22 @@ +This algorithm is used to find the minimum-cost-maximum-matching in a bipartite graph. + +## Usage + +1. implement the [IGraphDiffContext](./context/GraphDiffContext.ts) +2. implement the [IGraphDiffResolver](./resolver/BaseGraphDiffResolver.ts) + + +```typescript + +const context: IGraphDiffContext; +const resolver: IGraphDiffResolver; + +// Find the best bipartite graph matching. +const candidateMappings = resolver.findCandidateMapping(context); +const mappings = findBipartiteGraphMaxMapping(candidateMappings); +// You can perform additional matches and modify mappings. + +// Build diff graph. +context.buildDiffGraph(mappings, abOnlyNodes,) + +```