-
Notifications
You must be signed in to change notification settings - Fork 32
Expand file tree
/
Copy pathagglomerate_mapping_helper.ts
More file actions
217 lines (187 loc) · 7.52 KB
/
agglomerate_mapping_helper.ts
File metadata and controls
217 lines (187 loc) · 7.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
import cloneDeep from "lodash-es/cloneDeep";
import uniq from "lodash-es/uniq";
export class AgglomerateMapping {
/*
* This class is used to mock the backend in the proofreading
* tests. The class maintains an agglomerate mapping which consists
* of segment ids (nodes) and edges. The components in that graph
* define to which agglomerate ids segment ids are mapped.
* The class supports adding and removing edges. Each operation
* creates a new version so that map requests can be paramaterized
* with the version.
*/
public segmentIds: number[];
// adjacency list of the *current* graph (edges themselves are *not* versioned)
private readonly adjacencyList = new Map<number, Set<number>>();
// snapshot of the component‑ID map after every operation, versions[0] = initial state
private versions: Array<Map<number, number>> = [];
public currentVersion = -1; // newest version index
private largestMappedId: number; // monotone counter for fresh IDs
constructor(
public readonly edges: Array<[number, number]>,
initialVersion: number = 0,
) {
this.segmentIds = uniq(edges.flat());
this.largestMappedId = Math.max(...this.segmentIds);
const initialVersionMap = new Map<number, number>();
for (const segmentId of this.segmentIds) {
this.adjacencyList.set(segmentId, new Set());
initialVersionMap.set(segmentId, segmentId); // each segment is its own component at v0
}
this.commit(initialVersionMap, true);
for (const edge of edges) {
this.addEdge(edge[0], edge[1], true);
}
this.resetVersionCounter(initialVersion);
}
addEdge(segmentIdA: number, segmentIdB: number, bumpVersion: boolean): void {
/*
* Add an edge and record the new version. All segment ids
* that are present in the component defined by segmentIdB
* are remapped to the mapped id of segmentIdA.
*/
this.ensureNode(segmentIdA);
this.ensureNode(segmentIdB);
this.adjacencyList.get(segmentIdA)!.add(segmentIdB);
this.adjacencyList.get(segmentIdB)!.add(segmentIdA);
const previousVersionMap = this.versions[this.currentVersion];
const nextVersionMap = new Map(previousVersionMap); // shallow copy for copy‑on‑write
const componentIdA = previousVersionMap.get(segmentIdA)!;
const componentIdB = previousVersionMap.get(segmentIdB)!;
// Only merge if the components differ
if (componentIdA !== componentIdB) {
for (const [segmentId, componentId] of nextVersionMap) {
if (componentId === componentIdB) nextVersionMap.set(segmentId, componentIdA);
}
}
this.commit(nextVersionMap, bumpVersion);
}
removeEdge(segmentIdA: number, segmentIdB: number, bumpVersion: boolean): void {
/*
* Remove an edge, possibly splitting a component, and record the new version.
* The source component (defined by segmentIdA) will keep its mapped id.
* The target component is reassigned to a new id.
*/
this.ensureNode(segmentIdA);
this.ensureNode(segmentIdB);
this.adjacencyList.get(segmentIdA)!.delete(segmentIdB);
this.adjacencyList.get(segmentIdB)!.delete(segmentIdA);
console.log("removing edge", segmentIdA, segmentIdB);
const previousVersionMap = this.versions[this.currentVersion];
const keepComponentId = previousVersionMap.get(segmentIdA)!;
const nextVersionMap = new Map<number, number>();
const visitedSegmentIds = new Set<number>();
for (const startSegmentId of this.segmentIds) {
if (visitedSegmentIds.has(startSegmentId)) continue;
// BFS to collect this component
const bfsStack = [startSegmentId];
const componentSegmentIds: number[] = [];
visitedSegmentIds.add(startSegmentId);
while (bfsStack.length) {
const currentSegmentId = bfsStack.pop()!;
componentSegmentIds.push(currentSegmentId);
for (const neighborSegmentId of this.adjacencyList.get(currentSegmentId) ?? []) {
if (!visitedSegmentIds.has(neighborSegmentId)) {
visitedSegmentIds.add(neighborSegmentId);
bfsStack.push(neighborSegmentId);
}
}
}
// Determine the ID to assign:
let newComponentId: number;
if (
componentSegmentIds.some(
(segmentId) => previousVersionMap.get(segmentId) !== previousVersionMap.get(segmentIdA),
)
) {
// Unrelated component → keep previous IDs
for (const segmentId of componentSegmentIds) {
nextVersionMap.set(segmentId, previousVersionMap.get(segmentId)!);
}
continue;
} else if (componentSegmentIds.includes(segmentIdA)) {
// Component that includes `segmentIdA` keeps its ID
newComponentId = keepComponentId;
} else {
// New component → assign fresh ID
newComponentId = ++this.largestMappedId;
}
for (const segmentId of componentSegmentIds) {
nextVersionMap.set(segmentId, newComponentId);
}
}
this.commit(nextVersionMap, bumpVersion);
}
mapSegment(segmentId: number, version: number): number {
/*
* Look up the component‑ID of `segmentId` at an arbitrary version.
*/
const versionMap = this.getMap(version);
const componentId = versionMap.get(segmentId);
if (componentId === undefined) throw new Error("Unknown segmentId");
return componentId;
}
getMap(version: number): Map<number, number> {
/*
* Get the entire mapping for a specific version.
*/
if (version == null || version < 0 || version > this.currentVersion)
throw new RangeError(`Invalid version: ${version}`);
return this.versions[version];
}
// Returns a copy of the current adjacency list. It is a deep clone to avoid direct manipulation from outside.
// AdjacencyList is currently unversioned.
getAdjacencyList(): Map<number, Set<number>> {
return cloneDeep(this.adjacencyList);
}
private resetVersionCounter(initialVersion: number) {
/*
* Reset the most recent version to be stored as version `initialVersion`.
* If `initialVersion` is greater than 0, all previous versions are set to the
* newest version.
*/
const newestMap = this.versions.at(-1);
if (newestMap == null) {
throw new Error("No initial version of map found.");
}
this.versions = Array.from({ length: initialVersion + 1 }, () => newestMap);
this.currentVersion = initialVersion;
}
bumpVersion() {
/*
* Increment the version even though nothing changed (necessary
* because the backend also stores other things than the agglomerate mapping
* in the annotation).
*/
const newestMap = this.versions.at(-1);
if (newestMap == null) {
throw new Error("No initial version of map found.");
}
this.commit(newestMap, true);
}
/* Helpers */
private ensureNode(segmentId: number): void {
if (!this.adjacencyList.has(segmentId)) {
throw new Error(`Segment ${segmentId} was not in the original set`);
}
}
private commit(newVersionMap: Map<number, number>, bumpVersion: boolean): void {
/*
* Mush mapping snapshot and advance the global version counter
*/
if (bumpVersion) {
this.currentVersion++;
this.versions.push(newVersionMap);
console.log(
`Committed v=${this.currentVersion} with mapping: `,
newVersionMap.entries().toArray(),
);
} else {
this.versions[this.currentVersion] = newVersionMap;
console.log(
`Appended new update to v=${this.currentVersion}; resulting mapping: `,
newVersionMap.entries().toArray(),
);
}
}
}