@milaboratories/pl-model-common 1.19.5 → 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.
- package/dist/drivers/pframe/data_types.d.ts +4 -4
- package/dist/drivers/pframe/data_types.d.ts.map +1 -1
- package/dist/drivers/pframe/index.d.ts +1 -0
- package/dist/drivers/pframe/index.d.ts.map +1 -1
- package/dist/drivers/pframe/linker_columns.d.ts +43 -0
- package/dist/drivers/pframe/linker_columns.d.ts.map +1 -0
- package/dist/drivers/pframe/spec/spec.d.ts +164 -12
- package/dist/drivers/pframe/spec/spec.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1013 -614
- package/dist/index.mjs.map +1 -1
- package/dist/json.d.ts +3 -3
- package/dist/json.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/drivers/pframe/data_types.ts +26 -29
- package/src/drivers/pframe/index.ts +2 -0
- package/src/drivers/pframe/linker_columns.test.ts +238 -0
- package/src/drivers/pframe/linker_columns.ts +283 -0
- package/src/drivers/pframe/spec/spec.test.ts +182 -0
- package/src/drivers/pframe/spec/spec.ts +404 -22
- package/src/json.ts +3 -3
package/dist/json.d.ts
CHANGED
|
@@ -3,14 +3,14 @@ type JsonValue = JsonPrimitive | JsonValue[] | {
|
|
|
3
3
|
[key: string]: JsonValue;
|
|
4
4
|
};
|
|
5
5
|
type NotAssignableToJson = bigint | symbol | Function;
|
|
6
|
-
export type JsonCompatible<T> = unknown extends T ?
|
|
6
|
+
export type JsonCompatible<T> = unknown extends T ? unknown : {
|
|
7
7
|
[P in keyof T]: T[P] extends JsonValue ? T[P] : T[P] extends NotAssignableToJson ? never : JsonCompatible<T[P]>;
|
|
8
8
|
};
|
|
9
|
-
export type StringifiedJson<T> = JsonCompatible<T> extends never ? never : string & {
|
|
9
|
+
export type StringifiedJson<T = unknown> = JsonCompatible<T> extends never ? never : string & {
|
|
10
10
|
__json_stringified: T;
|
|
11
11
|
};
|
|
12
12
|
export declare function stringifyJson<T>(value: JsonCompatible<T>): StringifiedJson<T>;
|
|
13
|
-
export type CanonicalizedJson<T> = JsonCompatible<T> extends never ? never : string & {
|
|
13
|
+
export type CanonicalizedJson<T = unknown> = JsonCompatible<T> extends never ? never : string & {
|
|
14
14
|
__json_canonicalized: T;
|
|
15
15
|
};
|
|
16
16
|
export declare function canonicalizeJson<T>(value: JsonCompatible<T>): CanonicalizedJson<T>;
|
package/dist/json.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../src/json.ts"],"names":[],"mappings":"AAEA,KAAK,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC;AAElE,KAAK,SAAS,GAAG,aAAa,GAAG,SAAS,EAAE,GAAG;IAC7C,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1B,CAAC;AAGF,KAAK,mBAAmB,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEtD,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI,OAAO,SAAS,CAAC,GAAG,
|
|
1
|
+
{"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../src/json.ts"],"names":[],"mappings":"AAEA,KAAK,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC;AAElE,KAAK,SAAS,GAAG,aAAa,GAAG,SAAS,EAAE,GAAG;IAC7C,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1B,CAAC;AAGF,KAAK,mBAAmB,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEtD,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI,OAAO,SAAS,CAAC,GAAG,OAAO,GAAG;KAC3D,CAAC,IAAI,MAAM,CAAC,GACb,CAAC,CAAC,CAAC,CAAC,SAAS,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,GAC3B,CAAC,CAAC,CAAC,CAAC,SAAS,mBAAmB,GAAG,KAAK,GACtC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,eAAe,CAAC,CAAC,GAAG,OAAO,IAAI,cAAc,CAAC,CAAC,CAAC,SAAS,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG;IAC5F,kBAAkB,EAAE,CAAC,CAAC;CACvB,CAAC;AAEF,wBAAgB,aAAa,CAAC,CAAC,EAAE,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,CAE7E;AAED,MAAM,MAAM,iBAAiB,CAAC,CAAC,GAAG,OAAO,IAAI,cAAc,CAAC,CAAC,CAAC,SAAS,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG;IAC9F,oBAAoB,EAAE,CAAC,CAAC;CACzB,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,iBAAiB,CAAC,CAAC,CAAC,CAElF;AAED,wBAAgB,SAAS,CAAC,CAAC,EAAE,KAAK,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,iBAAiB,CAAC,CAAC,CAAC,GAAG,CAAC,CAEhF"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import type { Branded } from '../../branding';
|
|
2
|
-
import {
|
|
3
|
-
type ValueType,
|
|
4
|
-
type ValueTypeBytes,
|
|
5
|
-
} from './spec/spec';
|
|
2
|
+
import { ValueType } from './spec/spec';
|
|
6
3
|
|
|
7
4
|
export type PVectorDataInt = Int32Array;
|
|
8
5
|
export type PVectorDataLong = BigInt64Array;
|
|
@@ -11,12 +8,12 @@ export type PVectorDataDouble = Float64Array;
|
|
|
11
8
|
export type PVectorDataString = (null | string)[];
|
|
12
9
|
export type PVectorDataBytes = (null | Uint8Array)[];
|
|
13
10
|
export type PVectorDataTyped<DataType extends ValueType> =
|
|
14
|
-
DataType extends
|
|
15
|
-
DataType extends
|
|
16
|
-
DataType extends
|
|
17
|
-
DataType extends
|
|
18
|
-
DataType extends
|
|
19
|
-
DataType extends
|
|
11
|
+
DataType extends typeof ValueType.Int ? PVectorDataInt :
|
|
12
|
+
DataType extends typeof ValueType.Long ? PVectorDataLong :
|
|
13
|
+
DataType extends typeof ValueType.Float ? PVectorDataFloat :
|
|
14
|
+
DataType extends typeof ValueType.Double ? PVectorDataDouble :
|
|
15
|
+
DataType extends typeof ValueType.String ? PVectorDataString :
|
|
16
|
+
DataType extends typeof ValueType.Bytes ? PVectorDataBytes :
|
|
20
17
|
never;
|
|
21
18
|
export type PVectorData = PVectorDataTyped<ValueType>;
|
|
22
19
|
|
|
@@ -62,17 +59,17 @@ function isValueNA(vector: PTableVector, row: number): boolean {
|
|
|
62
59
|
const valueType = vector.type;
|
|
63
60
|
const value = vector.data[row];
|
|
64
61
|
switch (valueType) {
|
|
65
|
-
case
|
|
62
|
+
case ValueType.Int:
|
|
66
63
|
return (value as PVectorDataInt[number]) === -2147483648;
|
|
67
|
-
case
|
|
64
|
+
case ValueType.Long:
|
|
68
65
|
return (value as PVectorDataLong[number]) === -9007199254740991n;
|
|
69
|
-
case
|
|
66
|
+
case ValueType.Float:
|
|
70
67
|
return Number.isNaN((value as PVectorDataFloat[number]));
|
|
71
|
-
case
|
|
68
|
+
case ValueType.Double:
|
|
72
69
|
return Number.isNaN((value as PVectorDataDouble[number]));
|
|
73
|
-
case
|
|
70
|
+
case ValueType.String:
|
|
74
71
|
return (value as PVectorDataString[number]) === null;
|
|
75
|
-
case
|
|
72
|
+
case ValueType.Bytes:
|
|
76
73
|
return (value as PVectorDataBytes[number]) === null;
|
|
77
74
|
default:
|
|
78
75
|
throw Error(`unsupported data type: ${valueType satisfies never}`);
|
|
@@ -95,7 +92,7 @@ export function isPTableNA(value: unknown): value is PTableNA {
|
|
|
95
92
|
return value === PTableNA;
|
|
96
93
|
}
|
|
97
94
|
|
|
98
|
-
export type ValueTypeSupported = Exclude<ValueType,
|
|
95
|
+
export type ValueTypeSupported = Exclude<ValueType, typeof ValueType.Bytes>;
|
|
99
96
|
|
|
100
97
|
export type PTableValueInt = number;
|
|
101
98
|
export type PTableValueLong = number;
|
|
@@ -103,11 +100,11 @@ export type PTableValueFloat = number;
|
|
|
103
100
|
export type PTableValueDouble = number;
|
|
104
101
|
export type PTableValueString = string;
|
|
105
102
|
export type PTableValueData<DataType extends ValueTypeSupported> =
|
|
106
|
-
DataType extends
|
|
107
|
-
DataType extends
|
|
108
|
-
DataType extends
|
|
109
|
-
DataType extends
|
|
110
|
-
DataType extends
|
|
103
|
+
DataType extends typeof ValueType.Int ? PTableValueInt :
|
|
104
|
+
DataType extends typeof ValueType.Long ? PTableValueLong :
|
|
105
|
+
DataType extends typeof ValueType.Float ? PTableValueFloat :
|
|
106
|
+
DataType extends typeof ValueType.Double ? PTableValueDouble :
|
|
107
|
+
DataType extends typeof ValueType.String ? PTableValueString :
|
|
111
108
|
never;
|
|
112
109
|
export type PTableValueDataBranded<DataType extends ValueTypeSupported> = Branded<PTableValueData<DataType>, DataType>;
|
|
113
110
|
export type PTableValue<
|
|
@@ -157,8 +154,8 @@ function pTableValueImpl<
|
|
|
157
154
|
dataType?: DataType;
|
|
158
155
|
},
|
|
159
156
|
) {
|
|
160
|
-
const valueType
|
|
161
|
-
if (valueType ===
|
|
157
|
+
const valueType = column.type;
|
|
158
|
+
if (valueType === ValueType.Bytes) {
|
|
162
159
|
throw Error('Bytes not yet supported');
|
|
163
160
|
}
|
|
164
161
|
|
|
@@ -176,15 +173,15 @@ function pTableValueImpl<
|
|
|
176
173
|
|
|
177
174
|
const value = column.data[row]!;
|
|
178
175
|
switch (valueType) {
|
|
179
|
-
case
|
|
176
|
+
case ValueType.Int:
|
|
180
177
|
return value as PVectorDataInt[number];
|
|
181
|
-
case
|
|
178
|
+
case ValueType.Long:
|
|
182
179
|
return Number(value as PVectorDataLong[number]);
|
|
183
|
-
case
|
|
180
|
+
case ValueType.Float:
|
|
184
181
|
return value as PVectorDataFloat[number];
|
|
185
|
-
case
|
|
182
|
+
case ValueType.Double:
|
|
186
183
|
return value as PVectorDataDouble[number];
|
|
187
|
-
case
|
|
184
|
+
case ValueType.String:
|
|
188
185
|
return (value as PVectorDataString[number])!;
|
|
189
186
|
}
|
|
190
187
|
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Annotation,
|
|
3
|
+
AxisSpec,
|
|
4
|
+
AxisSpecNormalized,
|
|
5
|
+
getArrayFromAxisTree,
|
|
6
|
+
getAxesTree,
|
|
7
|
+
getNormalizedAxesList,
|
|
8
|
+
getSetFromAxisTree,
|
|
9
|
+
PColumnIdAndSpec,
|
|
10
|
+
ValueType,
|
|
11
|
+
} from './spec/index';
|
|
12
|
+
import { PObjectId } from '../../pool';
|
|
13
|
+
import { stringifyJson } from '../../json'
|
|
14
|
+
import {
|
|
15
|
+
describe,
|
|
16
|
+
expect,
|
|
17
|
+
test,
|
|
18
|
+
} from 'vitest';
|
|
19
|
+
import { LinkerMap } from './linker_columns';
|
|
20
|
+
|
|
21
|
+
function makeTestAxis(params: {
|
|
22
|
+
name: string;
|
|
23
|
+
parents?: AxisSpec[];
|
|
24
|
+
}): AxisSpec {
|
|
25
|
+
return {
|
|
26
|
+
type: ValueType.Int,
|
|
27
|
+
name: params.name,
|
|
28
|
+
annotations: {
|
|
29
|
+
[Annotation.Label]: `${params.name} axis`,
|
|
30
|
+
...(params.parents && params.parents.length > 0
|
|
31
|
+
? { [Annotation.Parents]: stringifyJson(params.parents) }
|
|
32
|
+
: {}
|
|
33
|
+
),
|
|
34
|
+
} satisfies Annotation,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makeLinkerColumn(params: {
|
|
39
|
+
name: string;
|
|
40
|
+
from: AxisSpec[];
|
|
41
|
+
to: AxisSpec[];
|
|
42
|
+
}): PColumnIdAndSpec {
|
|
43
|
+
return {
|
|
44
|
+
columnId: params.name as PObjectId,
|
|
45
|
+
spec: {
|
|
46
|
+
kind: 'PColumn',
|
|
47
|
+
valueType: ValueType.String,
|
|
48
|
+
name: params.name,
|
|
49
|
+
axesSpec: [...params.from, ...params.to],
|
|
50
|
+
annotations: {
|
|
51
|
+
[Annotation.Label]: `${params.name} column`,
|
|
52
|
+
[Annotation.IsLinkerColumn]: stringifyJson(true),
|
|
53
|
+
} satisfies Annotation,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Returns all permutations of initial array */
|
|
59
|
+
function allPermutations<T>(arr: T[]): T[][] {
|
|
60
|
+
switch (arr.length) {
|
|
61
|
+
case 0: return [];
|
|
62
|
+
case 1: return [arr];
|
|
63
|
+
case 2: return [arr, [arr[1], arr[0]]];
|
|
64
|
+
default: return arr.reduce(
|
|
65
|
+
(acc, item, i) => acc.concat(
|
|
66
|
+
allPermutations<T>([...arr.slice(0, i), ...arr.slice(i + 1)])
|
|
67
|
+
.map(val => [item, ...val])
|
|
68
|
+
),
|
|
69
|
+
[] as T[][],
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
describe('Linker columns', () => {
|
|
75
|
+
test('Search in linker columns map', () => {
|
|
76
|
+
const [axis1, axis2, axis3, axis4, axis5] = getNormalizedAxesList([
|
|
77
|
+
makeTestAxis({ name: 'id1' }),
|
|
78
|
+
makeTestAxis({ name: 'id2' }),
|
|
79
|
+
makeTestAxis({ name: 'id3' }),
|
|
80
|
+
makeTestAxis({ name: 'id4' }),
|
|
81
|
+
makeTestAxis({ name: 'id5' })
|
|
82
|
+
]);
|
|
83
|
+
const linkerMap = LinkerMap.fromColumns([
|
|
84
|
+
makeLinkerColumn({ name: 'c12', from: [axis1], to: [axis2] }),
|
|
85
|
+
makeLinkerColumn({ name: 'c13', from: [axis1], to: [axis3] }),
|
|
86
|
+
makeLinkerColumn({ name: 'c45', from: [axis4], to: [axis5] }),
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
let testCase = (params: {
|
|
90
|
+
from: AxisSpecNormalized[];
|
|
91
|
+
to: AxisSpecNormalized[];
|
|
92
|
+
expected: string[];
|
|
93
|
+
}) => {
|
|
94
|
+
const linkers = linkerMap.getLinkerColumnsForAxes({
|
|
95
|
+
from: params.from,
|
|
96
|
+
to: params.to,
|
|
97
|
+
throwWhenNoLinkExists: false,
|
|
98
|
+
});
|
|
99
|
+
expect(linkers.map(item => item.spec.name).sort()).toEqual(params.expected);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
testCase({ from: [axis2], to: [axis3], expected: ['c12', 'c13'] });
|
|
103
|
+
testCase({ from: [axis1], to: [axis2], expected: ['c12'] });
|
|
104
|
+
testCase({ from: [axis1], to: [axis4], expected: []});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('Axis tree - without parents', () => {
|
|
108
|
+
const [axisA, axisB] = getNormalizedAxesList([
|
|
109
|
+
makeTestAxis({ name: 'a' }),
|
|
110
|
+
makeTestAxis({ name: 'b' })
|
|
111
|
+
]);
|
|
112
|
+
const tree = getAxesTree(axisA);
|
|
113
|
+
expect(getSetFromAxisTree(tree).size).toBe(1);
|
|
114
|
+
expect(getArrayFromAxisTree(tree).length).toBe(1);
|
|
115
|
+
|
|
116
|
+
expect(LinkerMap.getAxesGroups([axisA, axisB]).length).toBe(2);
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('Axis tree - with parents', () => {
|
|
120
|
+
const axisD = makeTestAxis({ name: 'd' });
|
|
121
|
+
const axisC = makeTestAxis({ name: 'c', parents: [axisD] });
|
|
122
|
+
const axisB = makeTestAxis({ name: 'b', parents: [axisC] });
|
|
123
|
+
const axisA = makeTestAxis({ name: 'a', parents: [axisB] });
|
|
124
|
+
const [axisDn, axisCn, axisBn, axisAn] = getNormalizedAxesList([axisD, axisC, axisB, axisA])
|
|
125
|
+
|
|
126
|
+
const tree = getAxesTree(axisAn);
|
|
127
|
+
expect(getSetFromAxisTree(tree).size).toBe(4);
|
|
128
|
+
expect(getArrayFromAxisTree(tree).length).toBe(4);
|
|
129
|
+
|
|
130
|
+
for (const group of allPermutations([axisAn, axisBn, axisCn, axisDn])) {
|
|
131
|
+
expect(LinkerMap.getAxesGroups(group).length).toBe(1);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const axisD2 = makeTestAxis({ name: 'd' });
|
|
135
|
+
const axisC2 = makeTestAxis({ name: 'c', parents: [axisD2] });
|
|
136
|
+
const axisB2 = makeTestAxis({ name: 'b' });
|
|
137
|
+
const axisA2 = makeTestAxis({ name: 'a', parents: [axisB2] });
|
|
138
|
+
const normalized2 = getNormalizedAxesList([axisD2, axisC2, axisB2, axisA2])
|
|
139
|
+
|
|
140
|
+
for (const group of allPermutations(normalized2)) {
|
|
141
|
+
expect(LinkerMap.getAxesGroups(group).length).toBe(2);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const axisD3 = makeTestAxis({ name: 'd' });
|
|
145
|
+
const axisC3 = makeTestAxis({ name: 'c' });
|
|
146
|
+
const axisB3 = makeTestAxis({ name: 'b' });
|
|
147
|
+
const axisA3 = makeTestAxis({ name: 'a', parents: [axisB3] });
|
|
148
|
+
const normalized3 = getNormalizedAxesList([axisD3, axisC3, axisB3, axisA3])
|
|
149
|
+
|
|
150
|
+
for (const group of allPermutations(normalized3)) {
|
|
151
|
+
expect(LinkerMap.getAxesGroups(group).length).toBe(3);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const axisD4 = makeTestAxis({ name: 'd' });
|
|
155
|
+
const axisC4 = makeTestAxis({ name: 'c' });
|
|
156
|
+
const axisB4 = makeTestAxis({ name: 'b' });
|
|
157
|
+
const axisA4 = makeTestAxis({ name: 'a' });
|
|
158
|
+
const normalized4 = getNormalizedAxesList([axisD4, axisC4, axisB4, axisA4])
|
|
159
|
+
|
|
160
|
+
for (const group of allPermutations(normalized4)) {
|
|
161
|
+
expect(LinkerMap.getAxesGroups(group).length).toBe(4);
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('Generate partial trees', () => {
|
|
166
|
+
// Axes graph of parents (A, E - roots, C, B, D - parents) in some column:
|
|
167
|
+
// A - C
|
|
168
|
+
// \_ B _ D
|
|
169
|
+
// E/
|
|
170
|
+
//
|
|
171
|
+
// If the column is not a linker: trees to search linkers should be:
|
|
172
|
+
// 1 C
|
|
173
|
+
// 2 D
|
|
174
|
+
// 3 B - D
|
|
175
|
+
// 4 A - C
|
|
176
|
+
// \_ B - D
|
|
177
|
+
// 5 E - B - D
|
|
178
|
+
|
|
179
|
+
// If the axes are in a linker: trees must be in the linkers map:
|
|
180
|
+
|
|
181
|
+
// 1 A - C
|
|
182
|
+
// \_ B _ D
|
|
183
|
+
// 2 E - B - D
|
|
184
|
+
|
|
185
|
+
const axisD = makeTestAxis({ name: 'd' });
|
|
186
|
+
const axisC = makeTestAxis({ name: 'c' });
|
|
187
|
+
const axisB = makeTestAxis({ name: 'b', parents: [axisD] });
|
|
188
|
+
const axisA = makeTestAxis({ name: 'a', parents: [axisB, axisC] });
|
|
189
|
+
const axisE = makeTestAxis({ name: 'e', parents: [axisB, axisC] });
|
|
190
|
+
const axisF = makeTestAxis({ name: 'f' });
|
|
191
|
+
const axisH = makeTestAxis({ name: 'h' });
|
|
192
|
+
|
|
193
|
+
const group1 = [axisA, axisB, axisC, axisD, axisE];
|
|
194
|
+
const group2 = [axisF];
|
|
195
|
+
const group3 = [axisH];
|
|
196
|
+
const group1Normalized = getNormalizedAxesList(group1);
|
|
197
|
+
const group2Normalized = getNormalizedAxesList(group2);
|
|
198
|
+
const [axisAn, axisBn, axisCn, axisDn, axisEn] = group1Normalized;
|
|
199
|
+
|
|
200
|
+
const linker1 = makeLinkerColumn({ name: 'linker1', from: group1, to: group2 });
|
|
201
|
+
const linker2 = makeLinkerColumn({ name: 'linker2', from: group2, to: group3 });
|
|
202
|
+
|
|
203
|
+
const roots = LinkerMap.getAxesRoots(group1Normalized);
|
|
204
|
+
|
|
205
|
+
expect(roots).toEqual([axisAn, axisEn]);
|
|
206
|
+
|
|
207
|
+
const groups = LinkerMap.getAxesGroups([...group1Normalized, ...group2Normalized]);
|
|
208
|
+
expect(groups.length).toBe(2);
|
|
209
|
+
expect(groups[0]).toEqual(group1Normalized);
|
|
210
|
+
expect(groups[1]).toEqual(group2Normalized);
|
|
211
|
+
|
|
212
|
+
const linkersMap = LinkerMap.fromColumns([linker1, linker2]);
|
|
213
|
+
|
|
214
|
+
expect(
|
|
215
|
+
new Set(linkersMap.getReachableByLinkersAxesFromAxes(group2Normalized).map(a => a.name))
|
|
216
|
+
).toEqual(
|
|
217
|
+
new Set(([...group1, ...group3]).map(a => a.name))
|
|
218
|
+
);
|
|
219
|
+
expect(linkersMap.getReachableByLinkersAxesFromAxes([axisDn])).toEqual([]);
|
|
220
|
+
expect(linkersMap.getReachableByLinkersAxesFromAxes([axisBn])).toEqual([]);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('Order of parents should not matter', () => {
|
|
224
|
+
const axisA = makeTestAxis({ name: 'a' });
|
|
225
|
+
const axisB = makeTestAxis({ name: 'b' });
|
|
226
|
+
const axisC1 = makeTestAxis({ name: 'c', parents: [axisA, axisB] });
|
|
227
|
+
const axisC2 = makeTestAxis({ name: 'c', parents: [axisB, axisA] });
|
|
228
|
+
const axisD = makeTestAxis({ name: 'd' });
|
|
229
|
+
|
|
230
|
+
const [a, b, c1, c2, d] = getNormalizedAxesList([axisA, axisB, axisC1, axisC2, axisD]);
|
|
231
|
+
const linkerMap = LinkerMap.fromColumns([
|
|
232
|
+
makeLinkerColumn({ name: 'linker1', from: [axisA, axisB, axisC1], to: [axisD] })
|
|
233
|
+
]);
|
|
234
|
+
|
|
235
|
+
expect(linkerMap.getReachableByLinkersAxesFromAxes([c2])).not.toHaveLength(0);
|
|
236
|
+
expect(linkerMap.getReachableByLinkersAxesFromAxes([c1])).not.toHaveLength(0);
|
|
237
|
+
})
|
|
238
|
+
});
|
|
@@ -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
|
+
}
|