@milaboratories/pl-model-common 1.19.4 → 1.19.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,283 @@
1
+ import {
2
+ canonicalizeJson,
3
+ parseJson,
4
+ type CanonicalizedJson,
5
+ } from '../../json';
6
+ import {
7
+ Annotation,
8
+ readAnnotationJson,
9
+ type AxisSpec,
10
+ type PColumnIdAndSpec,
11
+ type AxisSpecNormalized,
12
+ type AxisId,
13
+ getAxisId,
14
+ getNormalizedAxesList,
15
+ getArrayFromAxisTree,
16
+ getAxesTree,
17
+ } from './spec/spec';
18
+
19
+ type LinkerKey = CanonicalizedJson<AxisId[]>;
20
+ export type CompositeLinkerMap = Map<
21
+ LinkerKey,
22
+ {
23
+ keyAxesSpec: AxisSpecNormalized[]; // axis specs - source for the key
24
+ linkWith: Map<LinkerKey, PColumnIdAndSpec>; // for every axis (possibly in group with parents - available by linkers another axes and corresponding linkers)
25
+ }
26
+ >;
27
+
28
+ interface LinkersData {
29
+ data: CompositeLinkerMap;
30
+ }
31
+ export class LinkerMap implements LinkersData {
32
+ /** Graph of linkers connected by axes (single or grouped by parents) */
33
+ readonly data: CompositeLinkerMap;
34
+
35
+ constructor(linkerMap: CompositeLinkerMap) {
36
+ this.data = linkerMap;
37
+ }
38
+
39
+ get keys() {
40
+ return this.data.keys();
41
+ }
42
+
43
+ get keyAxesIds() {
44
+ return [...this.data.keys()].map(parseJson);
45
+ }
46
+
47
+ static fromColumns(columns: PColumnIdAndSpec[]) {
48
+ const result: CompositeLinkerMap = new Map();
49
+ for (const linker of columns.filter((l) => !!readAnnotationJson(l.spec, Annotation.IsLinkerColumn))) {
50
+ const groups = LinkerMap.getAxesGroups(getNormalizedAxesList(linker.spec.axesSpec)); // split input axes into groups by parent links from annotation
51
+
52
+ if (groups.length !== 2) {
53
+ continue; // not a valid linker column
54
+ }
55
+ const [left, right] = groups;
56
+
57
+ // In case of group:
58
+ // A - C
59
+ // \_ B _ D
60
+ // E/
61
+ // put 2 variants as keys:
62
+ // A - C
63
+ // \_ B _ D
64
+ // and
65
+ // E - B - D
66
+ const leftKeyVariants: [LinkerKey, AxisSpecNormalized[]][] = LinkerMap.getAxesRoots(left).map((axis) => {
67
+ const axes = getArrayFromAxisTree(getAxesTree(axis));
68
+ const key = canonicalizeJson(axes.map(getAxisId));
69
+ return [key, axes];
70
+ });
71
+ const rightKeyVariants: [LinkerKey, AxisSpecNormalized[]][] = LinkerMap.getAxesRoots(right).map((axis) => {
72
+ const axes = getArrayFromAxisTree(getAxesTree(axis));
73
+ const key = canonicalizeJson(axes.map(getAxisId));
74
+ return [key, axes];
75
+ });
76
+
77
+ for (const [keyLeft, spec] of leftKeyVariants) {
78
+ if (!result.has(keyLeft)) {
79
+ result.set(keyLeft, { keyAxesSpec: spec, linkWith: new Map() });
80
+ }
81
+ }
82
+ for (const [keyRight, spec] of rightKeyVariants) {
83
+ if (!result.has(keyRight)) {
84
+ result.set(keyRight, { keyAxesSpec: spec, linkWith: new Map() });
85
+ }
86
+ }
87
+ for (const [keyLeft] of leftKeyVariants) {
88
+ for (const [keyRight] of rightKeyVariants) {
89
+ result.get(keyLeft)?.linkWith.set(keyRight, linker);
90
+ result.get(keyRight)?.linkWith.set(keyLeft, linker);
91
+ }
92
+ }
93
+ }
94
+ return new this(result);
95
+ }
96
+
97
+ /** Get all available nodes of linker graphs if start from sourceAxesKeys */
98
+ searchAvailableAxesKeys(sourceAxesKeys: LinkerKey[]): Set<LinkerKey> {
99
+ const startKeys = new Set(sourceAxesKeys);
100
+ const allAvailableKeys = new Set<LinkerKey>();
101
+ let nextKeys = sourceAxesKeys;
102
+ while (nextKeys.length) {
103
+ const next: LinkerKey[] = [];
104
+ for (const key of nextKeys) {
105
+ const node = this.data.get(key);
106
+ if (!node) continue;
107
+ for (const availableKey of node.linkWith.keys()) {
108
+ if (!allAvailableKeys.has(availableKey) && !startKeys.has(availableKey)) {
109
+ next.push(availableKey);
110
+ allAvailableKeys.add(availableKey);
111
+ }
112
+ }
113
+ }
114
+ nextKeys = next;
115
+ }
116
+ return allAvailableKeys;
117
+ }
118
+
119
+ /** Get all linker columns that are necessary to reach endKey from startKey */
120
+ searchLinkerPath(startKey: LinkerKey, endKey: LinkerKey): PColumnIdAndSpec[] {
121
+ const previous: Record<LinkerKey, LinkerKey> = {};
122
+ let nextIds = new Set([startKey]);
123
+ const visited = new Set([startKey]);
124
+ while (nextIds.size) {
125
+ const next = new Set<LinkerKey>();
126
+ for (const nextId of nextIds) {
127
+ const node = this.data.get(nextId);
128
+ if (!node) continue;
129
+ for (const availableId of node.linkWith.keys()) {
130
+ previous[availableId] = nextId;
131
+ if (availableId === endKey) {
132
+ const ids: LinkerKey[] = [];
133
+ let current = endKey;
134
+ while (previous[current] !== startKey) {
135
+ ids.push(current);
136
+ current = previous[current];
137
+ }
138
+ ids.push(current);
139
+ return ids.map((id: LinkerKey) => this.data.get(id)!.linkWith.get(previous[id])!);
140
+ } else if (!visited.has(availableId)) {
141
+ next.add(availableId);
142
+ visited.add(availableId);
143
+ }
144
+ }
145
+ }
146
+ nextIds = next;
147
+ }
148
+ return [];
149
+ }
150
+
151
+ getLinkerColumnsForAxes({
152
+ from: sourceAxes,
153
+ to: targetAxes,
154
+ throwWhenNoLinkExists = true,
155
+ }: {
156
+ from: AxisSpecNormalized[];
157
+ to: AxisSpecNormalized[];
158
+ throwWhenNoLinkExists?: boolean;
159
+ }): PColumnIdAndSpec[] {
160
+ // start keys - all possible keys in linker map using sourceAxes (for example, all axes of block's columns or all axes of columns in data-inputs)
161
+ const startKeys: LinkerKey[] = sourceAxes.map(LinkerMap.getLinkerKeyFromAxisSpec);
162
+
163
+ return Array.from(
164
+ new Map(
165
+ LinkerMap.getAxesRoots(targetAxes)
166
+ .map(LinkerMap.getLinkerKeyFromAxisSpec) // target keys contain all axes to be linked; if some of target axes has parents they must be in the key
167
+ .flatMap((targetKey) => {
168
+ const linkers = startKeys
169
+ .map((startKey) => this.searchLinkerPath(startKey, targetKey))
170
+ .reduce((shortestPath, path) => shortestPath.length && shortestPath.length < path.length ? shortestPath : path,
171
+ [] as PColumnIdAndSpec[])
172
+ .map((linker) => [linker.columnId, linker] as const);
173
+ if (!linkers.length && throwWhenNoLinkExists) {
174
+ throw Error(`Unable to find linker column for ${targetKey}`);
175
+ }
176
+ return linkers;
177
+ }),
178
+ ).values(),
179
+ );
180
+ }
181
+
182
+ /** Get list of axisSpecs from keys of linker columns map */
183
+ getAxesListFromKeysList(keys: LinkerKey[]): AxisSpecNormalized[] {
184
+ return Array.from(
185
+ new Map(
186
+ keys.flatMap((key) => this.data.get(key)?.keyAxesSpec ?? [])
187
+ .map((axis) => [canonicalizeJson(getAxisId(axis)), axis]),
188
+ ).values(),
189
+ );
190
+ }
191
+
192
+ /** Get axes of target axes that are impossible to be linked to source axes with current linker map */
193
+ getNonLinkableAxes(
194
+ sourceAxes: AxisSpecNormalized[],
195
+ targetAxes: AxisSpecNormalized[],
196
+ ): AxisSpecNormalized[] {
197
+ const startKeys = sourceAxes.map(LinkerMap.getLinkerKeyFromAxisSpec);
198
+ // target keys contain all axes to be linked; if some of target axes has parents they must be in the key
199
+ const missedTargetKeys = LinkerMap.getAxesRoots(targetAxes)
200
+ .map(LinkerMap.getLinkerKeyFromAxisSpec)
201
+ .filter((targetKey) =>
202
+ !startKeys.some((startKey) => this.searchLinkerPath(startKey, targetKey).length),
203
+ );
204
+ return this.getAxesListFromKeysList(missedTargetKeys);
205
+ }
206
+
207
+ /** Get all axes that can be connected to sourceAxes by linkers */
208
+ getReachableByLinkersAxesFromAxes(sourceAxes: AxisSpecNormalized[]): AxisSpec[] {
209
+ const startKeys = sourceAxes.map(LinkerMap.getLinkerKeyFromAxisSpec);
210
+ const availableKeys = this.searchAvailableAxesKeys(startKeys);
211
+ return this.getAxesListFromKeysList([...availableKeys]);
212
+ }
213
+
214
+ static getLinkerKeyFromAxisSpec(axis: AxisSpecNormalized): LinkerKey {
215
+ return canonicalizeJson(getArrayFromAxisTree(getAxesTree(axis)).map(getAxisId));
216
+ }
217
+
218
+ /** Split array of axes into several arrays by parents: axes of one group are parents for each other.
219
+ There are no order inside every group. */
220
+ static getAxesGroups(axesSpec: AxisSpecNormalized[]): AxisSpecNormalized[][] {
221
+ switch (axesSpec.length) {
222
+ case 0: return [];
223
+ case 1: return [[axesSpec[0]]];
224
+ default: break;
225
+ }
226
+
227
+ const axisKeys = axesSpec.map((spec) => canonicalizeJson(getAxisId(spec)));
228
+ const axisParentsIdxs = axesSpec.map(
229
+ (spec) => new Set(
230
+ spec.parentAxesSpec
231
+ .map((spec) => canonicalizeJson(getAxisId(spec)))
232
+ .map((el) => {
233
+ const idx = axisKeys.indexOf(el);
234
+ if (idx === -1) {
235
+ throw new Error(`malformed axesSpec: ${JSON.stringify(axesSpec)}, unable to locate parent ${el}`);
236
+ }
237
+ return idx;
238
+ }),
239
+ ),
240
+ );
241
+
242
+ const allIdxs = [...axesSpec.keys()];
243
+ const groups: number[][] = []; // groups of axis indexes
244
+
245
+ const usedIdxs = new Set<number>();
246
+ let nextFreeEl = allIdxs.find((idx) => !usedIdxs.has(idx));
247
+ while (nextFreeEl !== undefined) {
248
+ const currentGroup = [nextFreeEl];
249
+ usedIdxs.add(nextFreeEl);
250
+
251
+ let nextElsOfCurrentGroup = [nextFreeEl];
252
+ while (nextElsOfCurrentGroup.length) {
253
+ const next = new Set<number>();
254
+ for (const groupIdx of nextElsOfCurrentGroup) {
255
+ const groupElementParents = axisParentsIdxs[groupIdx];
256
+ allIdxs.forEach((idx) => {
257
+ if (idx === groupIdx || usedIdxs.has(idx)) {
258
+ return;
259
+ }
260
+ const parents = axisParentsIdxs[idx];
261
+ if (parents.has(groupIdx) || groupElementParents.has(idx)) {
262
+ currentGroup.push(idx);
263
+ next.add(idx);
264
+ usedIdxs.add(idx);
265
+ }
266
+ });
267
+ }
268
+ nextElsOfCurrentGroup = [...next];
269
+ }
270
+
271
+ groups.push([...currentGroup]);
272
+ nextFreeEl = allIdxs.find((idx) => !usedIdxs.has(idx));
273
+ };
274
+
275
+ return groups.map((group) => group.map((idx) => axesSpec[idx]));
276
+ }
277
+
278
+ /** Get all axes that are not parents of any other axis */
279
+ static getAxesRoots(axes: AxisSpecNormalized[]): AxisSpecNormalized[] {
280
+ const parentsSet = new Set(axes.flatMap((axis) => axis.parentAxesSpec).map((spec) => canonicalizeJson(getAxisId(spec))));
281
+ return axes.filter((axis) => !parentsSet.has(canonicalizeJson(getAxisId(axis))));
282
+ }
283
+ }
@@ -0,0 +1,182 @@
1
+ import {
2
+ Annotation,
3
+ AxisSpec,
4
+ AxisSpecNormalized,
5
+ getAxisId,
6
+ ValueType,
7
+ getDenormalizedAxesList,
8
+ getNormalizedAxesList,
9
+ canonicalizeAxisWithParents,
10
+ } from './spec';
11
+ import { canonicalizeJson, stringifyJson } from '../../../json'
12
+ import {
13
+ describe,
14
+ expect,
15
+ test,
16
+ } from 'vitest';
17
+
18
+ function makeTestAxis(params: {
19
+ name: string;
20
+ parents?: AxisSpec[];
21
+ }): AxisSpec {
22
+ return {
23
+ type: ValueType.Int,
24
+ name: params.name,
25
+ annotations: {
26
+ [Annotation.Label]: `${params.name} axis`,
27
+ ...(params.parents && params.parents.length > 0
28
+ ? { [Annotation.Parents]: stringifyJson(params.parents) }
29
+ : {}
30
+ ),
31
+ } satisfies Annotation,
32
+ };
33
+ }
34
+
35
+ function makeTestAxisWithParentIdxs(params: {
36
+ name: string;
37
+ parents?: number[];
38
+ }): AxisSpec {
39
+ return {
40
+ type: ValueType.Int,
41
+ name: params.name,
42
+ parentAxes: params.parents
43
+ };
44
+ }
45
+
46
+ describe('Linker columns', () => {
47
+ test('Normalization of axes with parents in indexes/annotations', () => {
48
+ function compareAxesLists(list1:AxisSpecNormalized[], list2:AxisSpecNormalized[]) {
49
+ list1.forEach((el1, idx) => {
50
+ const el2 = list2[idx];
51
+ const id1 = canonicalizeJson(getAxisId(el1));
52
+ const id2 = canonicalizeJson(getAxisId(el2));
53
+ expect(id1).toEqual(id2);
54
+
55
+ const parents1 = canonicalizeJson(el1.parentAxesSpec.map(getAxisId));
56
+ const parents2 = canonicalizeJson(el2.parentAxesSpec.map(getAxisId));
57
+ expect(parents1).toEqual(parents2);
58
+ })
59
+ }
60
+
61
+ // case 1
62
+ const normalized1 = getNormalizedAxesList([
63
+ makeTestAxisWithParentIdxs({ name: 'd' }),
64
+ makeTestAxisWithParentIdxs({ name: 'c', parents: [0] }), // parent D
65
+ makeTestAxisWithParentIdxs({ name: 'b' }),
66
+ makeTestAxisWithParentIdxs({ name: 'a', parents: [2] }) // parent B
67
+ ])
68
+
69
+ const axisD1 = makeTestAxis({ name: 'd' });
70
+ const axisC1 = makeTestAxis({ name: 'c', parents: [axisD1] });
71
+ const axisB1 = makeTestAxis({ name: 'b' });
72
+ const axisA1 = makeTestAxis({ name: 'a', parents: [axisB1] });
73
+ const normalized2 = getNormalizedAxesList([axisD1, axisC1, axisB1, axisA1]);
74
+
75
+ compareAxesLists(normalized1, normalized2);
76
+
77
+ // case 2
78
+ const normalized3 = getNormalizedAxesList([
79
+ makeTestAxisWithParentIdxs({ name: 'd' }),
80
+ makeTestAxisWithParentIdxs({ name: 'c' }),
81
+ makeTestAxisWithParentIdxs({ name: 'b', parents: [0] }), // parent D
82
+ makeTestAxisWithParentIdxs({ name: 'a', parents: [1, 2] }) // parents B C
83
+ ])
84
+
85
+ const axisD2 = makeTestAxis({ name: 'd' });
86
+ const axisC2 = makeTestAxis({ name: 'c' });
87
+ const axisB2 = makeTestAxis({ name: 'b', parents: [axisD2] });
88
+ const axisA2 = makeTestAxis({ name: 'a', parents: [axisB2, axisC2] });
89
+ const normalized4 = getNormalizedAxesList([axisD2, axisC2, axisB2, axisA2]);
90
+
91
+ compareAxesLists(normalized3, normalized4);
92
+
93
+ // case 3
94
+
95
+ const normalized5 = getNormalizedAxesList([
96
+ makeTestAxisWithParentIdxs({ name: 'e' }),
97
+ makeTestAxisWithParentIdxs({ name: 'd' }),
98
+ makeTestAxisWithParentIdxs({ name: 'c', parents: [1] }), // parent D
99
+ makeTestAxisWithParentIdxs({ name: 'b', parents: [2] }), // parent C
100
+ makeTestAxisWithParentIdxs({ name: 'a', parents: [3] }) // parent B
101
+ ])
102
+
103
+ const axisE3 = makeTestAxis({ name: 'e' });
104
+ const axisD3 = makeTestAxis({ name: 'd' });
105
+ const axisC3 = makeTestAxis({ name: 'c', parents: [axisD3]});
106
+ const axisB3 = makeTestAxis({ name: 'b', parents: [axisC3] });
107
+ const axisA3 = makeTestAxis({ name: 'a', parents: [axisB3] });
108
+ const normalized6 = getNormalizedAxesList([axisE3, axisD3, axisC3, axisB3, axisA3]);
109
+
110
+ compareAxesLists(normalized5, normalized6);
111
+ })
112
+
113
+ test('Sort parents during normalization - idxs', () => {
114
+ const sourceAxesList1 = [
115
+ makeTestAxisWithParentIdxs({ name: 'a', parents: [2, 3] }),
116
+ makeTestAxisWithParentIdxs({ name: 'a', parents: [3, 2] }),
117
+ makeTestAxisWithParentIdxs({ name: 'b', parents: [4] }),
118
+ makeTestAxisWithParentIdxs({ name: 'b', parents: [5] }),
119
+ makeTestAxisWithParentIdxs({ name: 'c1' }),
120
+ makeTestAxisWithParentIdxs({ name: 'c2' }),
121
+ ];
122
+ const normalized1 = getNormalizedAxesList(sourceAxesList1);
123
+ const a1 = normalized1[0];
124
+ const a2 = normalized1[1];
125
+ expect(canonicalizeAxisWithParents(a1)).toEqual(canonicalizeAxisWithParents(a2));
126
+
127
+
128
+ const axisC1 = makeTestAxis({ name: 'c1' });
129
+ const axisC2 = makeTestAxis({ name: 'c2' });
130
+ const axisB1 = makeTestAxis({ name: 'b', parents: [axisC1] });
131
+ const axisB2 = makeTestAxis({ name: 'b', parents: [axisC2] });
132
+ const sourceAxesList2 = [
133
+ makeTestAxis({ name: 'a', parents: [axisB1, axisB2] }),
134
+ makeTestAxis({ name: 'a', parents: [axisB2, axisB1] }),
135
+ axisB1,
136
+ axisB2,
137
+ axisC1,
138
+ axisC2
139
+ ];
140
+ const normalized2 = getNormalizedAxesList(sourceAxesList2);
141
+ const a3 = normalized2[0];
142
+ const a4 = normalized2[1];
143
+ expect(canonicalizeAxisWithParents(a3)).toEqual(canonicalizeAxisWithParents(a4));
144
+
145
+ expect(canonicalizeAxisWithParents(a1)).toEqual(canonicalizeAxisWithParents(a3));
146
+ expect(canonicalizeAxisWithParents(a1)).toEqual(canonicalizeAxisWithParents(a4));
147
+ })
148
+
149
+ test('Denormalization of axes list', () => {
150
+ const sourceAxesList = [
151
+ makeTestAxisWithParentIdxs({ name: 'e' }),
152
+ makeTestAxisWithParentIdxs({ name: 'd' }),
153
+ makeTestAxisWithParentIdxs({ name: 'c', parents: [1] }), // parent D
154
+ makeTestAxisWithParentIdxs({ name: 'b', parents: [2] }), // parent C
155
+ makeTestAxisWithParentIdxs({ name: 'a', parents: [3] }) // parent B
156
+ ];
157
+ const normalized = getNormalizedAxesList(sourceAxesList);
158
+ const denormalized = getDenormalizedAxesList(normalized);
159
+
160
+ sourceAxesList.forEach((el1, idx) => {
161
+ const el2 = denormalized[idx];
162
+ const id1 = canonicalizeJson(getAxisId(el1));
163
+ const id2 = canonicalizeJson(getAxisId(el2));
164
+ expect(id1).toEqual(id2);
165
+ expect(el1.parentAxes).toEqual(el2.parentAxes);
166
+ });
167
+ })
168
+
169
+ test('Remove parent cycles from normalized axes', () => {
170
+ const sourceAxesList = [
171
+ makeTestAxisWithParentIdxs({ name: 'a', parents: [1] }),
172
+ makeTestAxisWithParentIdxs({ name: 'b', parents: [2] }),
173
+ makeTestAxisWithParentIdxs({ name: 'c', parents: [0] })
174
+ ];
175
+ const normalized = getNormalizedAxesList(sourceAxesList);
176
+
177
+ const [a, b, c] = normalized;
178
+ expect(a.parentAxesSpec.length).toBe(0);
179
+ expect(b.parentAxesSpec.length).toBe(0);
180
+ expect(c.parentAxesSpec.length).toBe(0);
181
+ })
182
+ });