@morscherlab/mint-sdk 1.3.0-beta.9 → 1.3.1
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/__tests__/components/AutoGroupModal.anatomy.test.d.ts +1 -0
- package/dist/__tests__/components/AutoGroupModal.wiring.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/fieldEdits.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/golden.test.d.ts +1 -0
- package/dist/components/AutoGroupModal.adapter.d.ts +27 -4
- package/dist/components/SmartGroup.types.d.ts +22 -0
- package/dist/components/SmartGroupFieldRecipe.groups.d.ts +10 -2
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +3 -0
- package/dist/components/index.js +1 -1
- package/dist/{components-GDDvof8D.js → components-bbanLd1u.js} +510 -287
- package/dist/components-bbanLd1u.js.map +1 -0
- package/dist/composables/autoGroup/classify.d.ts +7 -1
- package/dist/composables/autoGroup/columns.d.ts +16 -1
- package/dist/composables/autoGroup/index.d.ts +5 -7
- package/dist/composables/autoGroup/tokenize.d.ts +36 -1
- package/dist/composables/index.js +2 -2
- package/dist/composables/useAutoGroup.d.ts +13 -2
- package/dist/{composables-D6uqp0UF.js → composables-BbHozlA7.js} +2 -2
- package/dist/{composables-D6uqp0UF.js.map → composables-BbHozlA7.js.map} +1 -1
- package/dist/index.js +3 -3
- package/dist/install.js +1 -1
- package/dist/permissions.js +2 -2
- package/dist/permissions.js.map +1 -1
- package/dist/styles.css +129 -96
- package/dist/types/auto-group.d.ts +1 -1
- package/dist/{validation-Dmp9Q5W0.js → validation-CukfLjy-.js} +323 -187
- package/dist/validation-CukfLjy-.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/components/AppTopBarPageSelector.test.ts +4 -4
- package/src/__tests__/components/AutoGroupModal.anatomy.test.ts +86 -0
- package/src/__tests__/components/AutoGroupModal.wiring.test.ts +53 -0
- package/src/__tests__/components/SampleSelector.test.ts +14 -16
- package/src/__tests__/components/SmartGroupFieldRecipe.groups.test.ts +23 -0
- package/src/__tests__/components/SmartGroupFieldRecipe.test.ts +176 -0
- package/src/__tests__/composables/autoGroup/columns.test.ts +100 -11
- package/src/__tests__/composables/autoGroup/fieldEdits.test.ts +74 -0
- package/src/__tests__/composables/autoGroup/fingerprint.test.ts +9 -0
- package/src/__tests__/composables/autoGroup/golden.test.ts +187 -0
- package/src/__tests__/composables/autoGroup/groupByOrder.test.ts +7 -7
- package/src/__tests__/composables/autoGroup/tokenLength.test.ts +7 -0
- package/src/__tests__/composables/autoGroup/tokenize.test.ts +78 -1
- package/src/__tests__/composables/useAutoGroup.test.ts +2 -1
- package/src/__tests__/fixtures/auto-group/share-exp021.txt +103 -0
- package/src/__tests__/fixtures/auto-group/share-exp031.txt +39 -0
- package/src/__tests__/fixtures/auto-group/share-exp114.txt +123 -0
- package/src/__tests__/fixtures/auto-group/share-exp214.txt +63 -0
- package/src/__tests__/utils/permissions.test.ts +15 -0
- package/src/components/AutoGroupModal.adapter.ts +80 -9
- package/src/components/AutoGroupModal.vue +41 -8
- package/src/components/SmartGroup.types.ts +23 -0
- package/src/components/SmartGroupFieldRecipe.groups.ts +14 -2
- package/src/components/SmartGroupFieldRecipe.vue +289 -169
- package/src/components/internal/AppTopBarPageSelectorInternal.vue +3 -6
- package/src/composables/autoGroup/classify.ts +26 -7
- package/src/composables/autoGroup/columns.ts +144 -36
- package/src/composables/autoGroup/compose.ts +8 -10
- package/src/composables/autoGroup/fingerprint.ts +1 -1
- package/src/composables/autoGroup/index.ts +5 -12
- package/src/composables/autoGroup/tokenLength.ts +8 -0
- package/src/composables/autoGroup/tokenize.ts +148 -1
- package/src/composables/autoGroup/vocab.json +5 -11
- package/src/composables/useAutoGroup.ts +92 -79
- package/src/permissions.ts +3 -2
- package/src/styles/components/app-page-selector.css +58 -14
- package/src/styles/components/smart-group.css +71 -82
- package/src/types/auto-group.ts +1 -0
- package/dist/components-GDDvof8D.js.map +0 -1
- package/dist/composables/autoGroup/replicatePreGroup.d.ts +0 -36
- package/dist/validation-Dmp9Q5W0.js.map +0 -1
- package/src/composables/autoGroup/replicatePreGroup.ts +0 -124
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { useAutoGroup } from '../../../composables/useAutoGroup'
|
|
3
|
+
|
|
4
|
+
function engineFor(samples: string[]) {
|
|
5
|
+
const engine = useAutoGroup()
|
|
6
|
+
engine.rawText.value = samples.join('\n')
|
|
7
|
+
engine.parseInput()
|
|
8
|
+
return engine
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Two tissue classes; fields 2-4 are factors in both.
|
|
12
|
+
const twoClasses = ['Liver', 'Kidney'].flatMap(tissue =>
|
|
13
|
+
['A', 'B'].flatMap(first => ['C', 'D'].flatMap(second => ['E', 'F'].map(third => `${tissue}_${first}_${second}_${third}_rep1`))),
|
|
14
|
+
)
|
|
15
|
+
const designWithReplicates = ['WT', 'KO'].flatMap(genotype =>
|
|
16
|
+
['Vehicle', 'Drug'].flatMap(treatment => ['1', '2', '3'].map(replicate => `${genotype}-${treatment}-${replicate}`)),
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
describe('useAutoGroup field edits', () => {
|
|
20
|
+
it('should name a column Field N when no name can be inferred', () => {
|
|
21
|
+
expect(engineFor(designWithReplicates).activeSchema.value?.columns[0].name).toBe('Field 1')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should rename the field in every class when renaming by field key', () => {
|
|
25
|
+
const engine = engineFor(twoClasses)
|
|
26
|
+
engine.renameGroupByField('Field 3', 'Dose')
|
|
27
|
+
const names = Object.values(engine.schemas.value).map(schema => schema.columns[2].displayName)
|
|
28
|
+
expect(names).toEqual(['Dose', 'Dose'])
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should keep the old name when another field already has the new name', () => {
|
|
32
|
+
const engine = engineFor(twoClasses)
|
|
33
|
+
engine.renameGroupByField('Field 3', 'Field 2')
|
|
34
|
+
expect(engine.activeSchema.value?.columns[2].displayName).toBeUndefined()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should keep the old name when the new name is blank', () => {
|
|
38
|
+
const engine = engineFor(twoClasses)
|
|
39
|
+
engine.renameGroupByField('Field 3', ' ')
|
|
40
|
+
expect(engine.activeSchema.value?.columns[2].displayName).toBeUndefined()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should use the new name in the sample metadata when a field is renamed', () => {
|
|
44
|
+
const engine = engineFor(designWithReplicates)
|
|
45
|
+
engine.renameGroupByField('Field 1', 'Genotype')
|
|
46
|
+
expect(engine.result.value.metadata[0].fields.Genotype).toBe('WT')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should report the number of groups that remain when a field is switched off', () => {
|
|
50
|
+
expect(engineFor(designWithReplicates).groupCountWithout('Field 2')).toBe(2)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should leave the grouping unchanged when it reports the count for a field', () => {
|
|
54
|
+
const engine = engineFor(designWithReplicates)
|
|
55
|
+
engine.groupCountWithout('Field 2')
|
|
56
|
+
expect(engine.result.value.experimentalGroups).toHaveLength(4)
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('useAutoGroup field edits across classes', () => {
|
|
61
|
+
// Two token-length classes. `Field 2` is a factor in the short names and a
|
|
62
|
+
// constant in the long names, which have their own factor `Field 3`.
|
|
63
|
+
const twoLengths = [
|
|
64
|
+
'Liver_A_rep1', 'Liver_A_rep2', 'Liver_B_rep1', 'Liver_B_rep2',
|
|
65
|
+
'Liver_c_P_rep1', 'Liver_c_P_rep2', 'Liver_c_Q_rep1', 'Liver_c_Q_rep2',
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
it('should keep the old name when a field of another class already has the new name', () => {
|
|
69
|
+
const engine = engineFor(twoLengths)
|
|
70
|
+
engine.renameGroupByField('Field 2', 'Field 3')
|
|
71
|
+
const renamed = Object.values(engine.schemas.value).flatMap(schema => schema.columns).filter(c => c.displayName)
|
|
72
|
+
expect(renamed).toEqual([])
|
|
73
|
+
})
|
|
74
|
+
})
|
|
@@ -120,3 +120,12 @@ describe('fingerprint round-trip', () => {
|
|
|
120
120
|
expect(five?.columns).toHaveLength(2)
|
|
121
121
|
})
|
|
122
122
|
})
|
|
123
|
+
|
|
124
|
+
describe('fingerprint of an overlay class', () => {
|
|
125
|
+
it('should restore a snapshot that holds a token length when the current overlay class has none', () => {
|
|
126
|
+
const column = { index: 0, name: 'A', sourceIndices: [0], uniqueValues: ['x'], cardinality: 1, role: 'constant' as const }
|
|
127
|
+
const saved = serializeFingerprint([{ classKind: 'iqc', tokenLength: 5, columns: [column], groupBy: [] }])
|
|
128
|
+
const restored = restoreFingerprint(saved, [{ classKind: 'iqc', columns: [column], groupBy: [] }])
|
|
129
|
+
expect(restored).toHaveLength(1)
|
|
130
|
+
})
|
|
131
|
+
})
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
import { useAutoGroup } from '../../../composables/useAutoGroup'
|
|
5
|
+
|
|
6
|
+
// End-to-end defaults of the paste path: sample names in, group names and sizes
|
|
7
|
+
// out. Fixtures `share-*.txt` are real batches from the lab's mass-spec share.
|
|
8
|
+
|
|
9
|
+
function engineFor(samples: string[]) {
|
|
10
|
+
const engine = useAutoGroup()
|
|
11
|
+
engine.rawText.value = samples.join('\n')
|
|
12
|
+
engine.parseInput()
|
|
13
|
+
return engine
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function sizes(groups: { name: string; samples: string[] }[] | undefined): Record<string, number> {
|
|
17
|
+
return Object.fromEntries((groups ?? []).map(group => [group.name, group.samples.length]))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const experimental = (samples: string[]) => sizes(engineFor(samples).result.value.experimentalGroups)
|
|
21
|
+
const qc = (samples: string[]) => sizes(engineFor(samples).result.value.qcGroups)
|
|
22
|
+
|
|
23
|
+
function fixture(name: string): string[] {
|
|
24
|
+
return readFileSync(resolve(__dirname, `../../fixtures/auto-group/${name}.txt`), 'utf-8').trim().split('\n')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const cross = (a: string[], b: string[], c: string[], sep: string): string[] =>
|
|
28
|
+
a.flatMap(x => b.flatMap(y => c.map(z => [x, y, z].join(sep))))
|
|
29
|
+
|
|
30
|
+
const liverBatch = [
|
|
31
|
+
'Ctrl_Liver_D1_Rep1', 'Ctrl_Liver_D1_Rep2',
|
|
32
|
+
'Treat_Liver_D3_Rep1', 'Treat_Liver_D3_Rep2',
|
|
33
|
+
'Batch_IQC_Rep1', 'Batch_IQC_Rep2', 'Batch_IQC_Rep3',
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const tracingBatch = [
|
|
37
|
+
'LT_13102025_212_WT_Glu_3_20_S_091123_T1_33', 'LT_13102025_213_WT_Glu_3_20_S_091123_T1_34',
|
|
38
|
+
'LT_13102025_214_KI_Glu_3_20_S_091123_T1_35', 'LT_13102025_215_KI_Glu_6_40_L_091123_T1_36',
|
|
39
|
+
'LT_13102025_216_WT_Glu_6_40_L_091123_T1_37', 'LT_13102025_217_WT_Glu_6_40_L_091123_T2_38',
|
|
40
|
+
'LT_13102025_218_KI_Glu_3_20_S_091123_T2_39', 'LT_13102025_219_KI_Glu_6_40_L_091123_T2_40',
|
|
41
|
+
'LT_13102025_220_WT_Glu_3_20_S_091123_T2_41', 'LT_13102025_221_WT_Glu_6_40_L_091123_T2_42',
|
|
42
|
+
'LT_13102025_EQC_Jurkat_1_2', 'LT_13102025_EQC_Jurkat_2_8', 'LT_13102025_IQC_Pool_1_5',
|
|
43
|
+
'test_EQC_Jurkat_02', 'test_Blank_Run_01',
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
const twoMatrices = [
|
|
47
|
+
'Ctrl_Liver_D1_Rep1', 'Ctrl_Liver_D1_Rep2', 'Treat_Liver_D3_Rep1', 'Treat_Liver_D3_Rep2',
|
|
48
|
+
'QC_Pool', 'Blank', 'ISTD_Mix',
|
|
49
|
+
'Ctrl_Brain_D1_Rep1', 'Ctrl_Brain_D1_Rep2',
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
describe('auto-group defaults: replicates', () => {
|
|
53
|
+
it('should keep a trailing counter out of the group key when it repeats', () => {
|
|
54
|
+
expect(experimental(cross(['WT', 'KO'], ['Vehicle', 'Drug'], ['1', '2', '3'], '-'))).toEqual({
|
|
55
|
+
'WT / Vehicle': 3, 'WT / Drug': 3, 'KO / Vehicle': 3, 'KO / Drug': 3,
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should keep a trailing letter counter out of the group key when it repeats', () => {
|
|
60
|
+
expect(experimental(cross(['WT', 'KO'], ['Vehicle', 'Drug'], ['A', 'B', 'C'], '_'))).toEqual({
|
|
61
|
+
'WT / Vehicle': 3, 'WT / Drug': 3, 'KO / Vehicle': 3, 'KO / Drug': 3,
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should keep sample numbers, replicate tokens and injection numbers out of the group key when a name carries all three', () => {
|
|
66
|
+
expect(experimental(tracingBatch)).toEqual({
|
|
67
|
+
'WT / 3 / 20 / S': 3, 'KI / 3 / 20 / S': 2, 'KI / 6 / 40 / L': 2, 'WT / 6 / 40 / L': 3,
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should report original sample names in metadata when replicates collapse', () => {
|
|
72
|
+
expect(engineFor(liverBatch).result.value.metadata.map(row => row.sampleName)).toEqual(liverBatch)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should report original sample names as excluded when a class is excluded', () => {
|
|
76
|
+
const engine = engineFor(liverBatch)
|
|
77
|
+
engine.setClassDispositions(c => c.kind === 'iqc', 'exclude')
|
|
78
|
+
expect(engine.result.value.excludedSamples).toEqual(['Batch_IQC_Rep1', 'Batch_IQC_Rep2', 'Batch_IQC_Rep3'])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should count each QC replicate as a class member when QC names differ only by replicate', () => {
|
|
82
|
+
const iqc = engineFor(liverBatch).classes.value.find(c => c.kind === 'iqc')
|
|
83
|
+
expect(iqc?.members.length).toBe(3)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should fill each pre-filled template row with the tokens of its own sample when replicates collapse', () => {
|
|
87
|
+
const { content } = engineFor(liverBatch).composeTemplateForTest({ mode: 'prefilled', format: 'csv' })
|
|
88
|
+
const row = content.split('\n').find(line => line.startsWith('Treat_Liver_D3_Rep1,'))
|
|
89
|
+
expect(row).toContain('Treat / D3')
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('auto-group defaults: classes', () => {
|
|
94
|
+
it('should route bare QC and ISTD tokens to overlay classes when a name carries them', () => {
|
|
95
|
+
expect(Object.keys(qc(twoMatrices)).sort()).toEqual(['Blank', 'IQC', 'Standard'])
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('should start each group name with the class layer when the batch has two matrices', () => {
|
|
99
|
+
expect(experimental(twoMatrices)).toEqual({ 'Liver / Ctrl / D1': 2, 'Liver / Treat / D3': 2, 'Brain': 2 })
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('should keep one overlay group when a QC class has two token lengths', () => {
|
|
103
|
+
expect(qc(tracingBatch)).toEqual({ EQC: 3, IQC: 1, Blank: 1 })
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should not name a column Username when it holds two genotypes', () => {
|
|
107
|
+
const engine = engineFor(cross(['WT', 'KO'], ['Vehicle', 'Drug'], ['1', '2', '3'], '-'))
|
|
108
|
+
expect(engine.activeSchema.value?.columns.map(column => column.name)).not.toContain('Username')
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe('auto-group defaults: tokenizer', () => {
|
|
113
|
+
const pairs = { WT: 2, KO: 2 }
|
|
114
|
+
|
|
115
|
+
it('should drop a file extension when all names share it', () => {
|
|
116
|
+
expect(experimental(['WT_Ctrl_01.raw', 'WT_Ctrl_02.raw', 'KO_Ctrl_03.raw', 'KO_Ctrl_04.raw'])).toEqual(pairs)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('should split on spaces when no other delimiter is present', () => {
|
|
120
|
+
expect(experimental(['WT Ctrl 1', 'WT Ctrl 2', 'KO Ctrl 1', 'KO Ctrl 2'])).toEqual(pairs)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('should read one delimiter when it repeats', () => {
|
|
124
|
+
expect(experimental(['WT__Ctrl_1', 'WT__Ctrl_2', 'KO__Ctrl_1', 'KO__Ctrl_2'])).toEqual(pairs)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('should report the experiment header when all names share it', () => {
|
|
128
|
+
expect(engineFor(fixture('share-exp031')).experiment.value).toEqual({
|
|
129
|
+
code: 'exp031', date: '2025-03-18', operator: 'SU',
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe('auto-group defaults: lab share batches', () => {
|
|
135
|
+
it('should group exp031 into 8 conditions when the replicate is a letter', () => {
|
|
136
|
+
expect(experimental(fixture('share-exp031'))).toEqual({
|
|
137
|
+
'105 / DMSO': 4, '105 / TFP': 4, '270 / DMSO': 4, '270 / TFP': 4,
|
|
138
|
+
'SF8628 / DMSO': 4, 'SF8628 / TFP': 4, 'SU-VI / DMSO': 4, 'SU-VI / TFP': 4,
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('should group exp114 by condition when a token holds condition and replicate', () => {
|
|
143
|
+
expect(experimental(fixture('share-exp114'))['HMW / 2']).toBe(3)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('should group exp214 by stem when a counter is attached to it', () => {
|
|
147
|
+
expect(experimental(fixture('share-exp214'))['D1 / A']).toBe(3)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('should group exp021 by time point when it has more than five levels', () => {
|
|
151
|
+
expect(Object.keys(experimental(fixture('share-exp021')))).toContain('inf2 / 1 / 0min')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('should keep the label token B13 as a column when exp021 is parsed', () => {
|
|
155
|
+
const schemas = Object.values(engineFor(fixture('share-exp021')).schemas.value)
|
|
156
|
+
expect(schemas.flatMap(schema => schema.columns.flatMap(column => column.uniqueValues))).toContain('B13')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('should read no QC class when the word is in the experiment title that all names share', () => {
|
|
160
|
+
const names = cross(['WT', 'KO'], ['Vehicle', 'Drug'], ['1', '2', '3'], '_')
|
|
161
|
+
.map(body => `exp137_250919_AK_betaine_std_HILIC_${body}`)
|
|
162
|
+
expect(qc(names)).toEqual({})
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
describe('auto-group defaults: second review', () => {
|
|
167
|
+
it('should keep a QC-only batch as one overlay when QC sits where the operator is expected', () => {
|
|
168
|
+
expect(qc(['exp021_250101_QC_1', 'exp021_250101_QC_2'])).toEqual({ IQC: 2 })
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('should keep a QC-only batch as one overlay when all names share the QC token after the header', () => {
|
|
172
|
+
expect(qc(['exp021_250101_SU_QC_1', 'exp021_250101_SU_QC_2'])).toEqual({ IQC: 2 })
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('should group by a bare-number dose when it is the last token of three samples', () => {
|
|
176
|
+
expect(experimental(['Drug_1', 'Drug_5', 'Drug_10'])).toEqual({ '1': 1, '5': 1, '10': 1 })
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('should drop known file extensions when the names carry two different ones', () => {
|
|
180
|
+
expect(experimental(['WT_Ctrl_1.raw', 'WT_Ctrl_2.mzML', 'KO_Ctrl_1.raw', 'KO_Ctrl_2.mzML'])).toEqual({ WT: 2, KO: 2 })
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('should leave a letter counter out of the group key when it sits before an unpadded injection number', () => {
|
|
184
|
+
const names = cross(['WT', 'KO'], ['A', 'B', 'C'], ['x'], '_').map((name, i) => name.replace('_x', `_${i + 5}`))
|
|
185
|
+
expect(experimental(names)).toEqual({ WT: 3, KO: 3 })
|
|
186
|
+
})
|
|
187
|
+
})
|
|
@@ -111,7 +111,7 @@ describe('useAutoGroup — field edits span every class exposing the field', ()
|
|
|
111
111
|
|
|
112
112
|
it('should reorder the layer in every class when moving a field by name', () => {
|
|
113
113
|
const auto = twoClassEngine()
|
|
114
|
-
auto.moveGroupByField('
|
|
114
|
+
auto.moveGroupByField('Field 3', 'up')
|
|
115
115
|
expect(Object.values(auto.schemas.value).map(s => s.groupBy)).toEqual([[2, 1], [2, 1]])
|
|
116
116
|
})
|
|
117
117
|
|
|
@@ -120,7 +120,7 @@ describe('useAutoGroup — field edits span every class exposing the field', ()
|
|
|
120
120
|
const activeKey = auto.activeClassKey.value
|
|
121
121
|
auto.toggleGroupBy(activeKey, 2)
|
|
122
122
|
|
|
123
|
-
auto.moveGroupByField('
|
|
123
|
+
auto.moveGroupByField('Field 3', 'up')
|
|
124
124
|
|
|
125
125
|
expect(Object.values(auto.schemas.value).map(schema => schema.groupBy)).toEqual([[1], [2, 1]])
|
|
126
126
|
})
|
|
@@ -131,7 +131,7 @@ describe('useAutoGroup — field edits span every class exposing the field', ()
|
|
|
131
131
|
auto.moveGroupBy(inactiveKey, 3, 'up')
|
|
132
132
|
auto.moveGroupBy(inactiveKey, 3, 'up')
|
|
133
133
|
|
|
134
|
-
auto.moveGroupByField('
|
|
134
|
+
auto.moveGroupByField('Field 4', 'up')
|
|
135
135
|
|
|
136
136
|
expect(Object.values(auto.schemas.value).map(schema => schema.groupBy)).toEqual([
|
|
137
137
|
[1, 3, 2],
|
|
@@ -162,14 +162,14 @@ describe('useAutoGroup — field edits span every class exposing the field', ()
|
|
|
162
162
|
},
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
auto.moveGroupByField('
|
|
165
|
+
auto.moveGroupByField('Field 4', 'up')
|
|
166
166
|
|
|
167
167
|
expect(auto.schemas.value[inactiveKey].groupBy).toEqual([1, 3, 2, 99])
|
|
168
168
|
})
|
|
169
169
|
|
|
170
170
|
it('should compose consistently ordered names across classes after a field move', () => {
|
|
171
171
|
const auto = twoClassEngine()
|
|
172
|
-
auto.moveGroupByField('
|
|
172
|
+
auto.moveGroupByField('Field 3', 'up')
|
|
173
173
|
expect(auto.groups.value.map(g => g.name).sort()).toEqual([
|
|
174
174
|
'Kidney / High / 10min',
|
|
175
175
|
'Kidney / High / 5min',
|
|
@@ -218,8 +218,8 @@ describe('useAutoGroup — field edits span every class exposing the field', ()
|
|
|
218
218
|
},
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
auto.toggleGroupByField('
|
|
222
|
-
auto.toggleGroupByField('
|
|
221
|
+
auto.toggleGroupByField('Field 3')
|
|
222
|
+
auto.toggleGroupByField('Field 3')
|
|
223
223
|
|
|
224
224
|
expect(Object.values(auto.schemas.value).map(schema => schema.groupBy)).toEqual([[1], [1, 2]])
|
|
225
225
|
})
|
|
@@ -83,3 +83,10 @@ describe('splitByTokenLength', () => {
|
|
|
83
83
|
expect(out.map(c => c.label)).toEqual(['Unknown', 'Biological / Plasma'])
|
|
84
84
|
})
|
|
85
85
|
})
|
|
86
|
+
|
|
87
|
+
describe('splitByTokenLength for QC-kind classes', () => {
|
|
88
|
+
it('should keep a QC-kind class whole when its members have two token lengths', () => {
|
|
89
|
+
const iqc = cls({ kind: 'iqc', label: 'IQC', subKind: undefined, members: [0, 1] })
|
|
90
|
+
expect(splitByTokenLength([iqc], [['IQC', '1'], ['x', 'IQC', '2']])).toHaveLength(1)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { splitMulti, pickPrimaryDelimiter } from '../../../composables/autoGroup/tokenize'
|
|
2
|
+
import { splitMulti, pickPrimaryDelimiter, parseDateToken, tokenizeBatch } from '../../../composables/autoGroup/tokenize'
|
|
3
3
|
|
|
4
4
|
describe('pickPrimaryDelimiter', () => {
|
|
5
5
|
it('should pick underscore when underscore field counts are most consistent', () => {
|
|
@@ -31,3 +31,80 @@ describe('splitMulti', () => {
|
|
|
31
31
|
expect(splitMulti('', '_')).toEqual([''])
|
|
32
32
|
})
|
|
33
33
|
})
|
|
34
|
+
|
|
35
|
+
describe('parseDateToken', () => {
|
|
36
|
+
it('should read the date when the token is YYMMDD', () => {
|
|
37
|
+
expect(parseDateToken('250919')).toBe('2025-09-19')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should read the date when the token is YYYYMMDD', () => {
|
|
41
|
+
expect(parseDateToken('20250211')).toBe('2025-02-11')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should read DDMMYYYY when the first four digits are not a year', () => {
|
|
45
|
+
expect(parseDateToken('13102025')).toBe('2025-10-13')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should read DDMMYY when the year-first reading is outside the plausible window', () => {
|
|
49
|
+
expect(parseDateToken('091123')).toBe('2023-11-09')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should return undefined when the token is not a date', () => {
|
|
53
|
+
expect(parseDateToken('999999')).toBeUndefined()
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('tokenizeBatch', () => {
|
|
58
|
+
it('should move the header out of the rows when all names share it', () => {
|
|
59
|
+
const batch = tokenizeBatch(['exp031_250318_SU_105_DMSO_A_23', 'exp031_250318_SU_270_TFP_B_09'])
|
|
60
|
+
expect(batch.rows).toEqual([['105', 'DMSO', 'A', '23'], ['270', 'TFP', 'B', '09']])
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should read the header when the operator is before the date', () => {
|
|
64
|
+
const batch = tokenizeBatch(['exp157_LT_13102025_214_KI_13', 'exp157_LT_13102025_215_WT_35'])
|
|
65
|
+
expect(batch.experiment).toEqual({ code: 'exp157', date: '2025-10-13', operator: 'LT' })
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should keep a header field in the row when the batch has two values', () => {
|
|
69
|
+
const batch = tokenizeBatch(['exp280_260413_YS_Plasma_M10_031', 'exp281_260413_YS_Tissues_M10_085'])
|
|
70
|
+
expect(batch.rows[0]).toEqual(['exp280', 'Plasma', 'M10', '031'])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should read no header when a name has no experiment code and no date', () => {
|
|
74
|
+
expect(tokenizeBatch(['WT_Ctrl_1', 'KO_Ctrl_2']).rows[0]).toEqual(['WT', 'Ctrl', '1'])
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should drop the last token when it is a 14-digit acquisition stamp', () => {
|
|
78
|
+
const batch = tokenizeBatch(['WT_blank_011_20251003200301', 'KO_blank_012'])
|
|
79
|
+
expect(batch.rows[0]).toEqual(['WT', 'blank', '011'])
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should count the shared tokens when all names share a title after the header', () => {
|
|
83
|
+
const batch = tokenizeBatch(['exp114_250730_YY_HEK_Put_HMW_1-1_35', 'exp114_250730_YY_HEK_Put_MMW_2-1_36'])
|
|
84
|
+
expect(batch.sharedPrefix).toBe(2)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should keep a decimal point inside a token when the delimiter is an underscore', () => {
|
|
88
|
+
expect(tokenizeBatch(['exp_drugA_0.1uM', 'exp_drugB_1uM']).rows[0]).toEqual(['exp', 'drugA', '0.1uM'])
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('tokenizeBatch review regressions', () => {
|
|
93
|
+
it('should keep a caps token in the row when only some names carry it after the date', () => {
|
|
94
|
+
const batch = tokenizeBatch(['exp021_250101_QC_1', 'exp021_250101_QC_2', 'exp021_250101_Ctrl_1', 'exp021_250101_Ctrl_2'])
|
|
95
|
+
expect(batch.rows[0]).toEqual(['QC', '1'])
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('should keep rows aligned when one genotype looks like operator initials', () => {
|
|
99
|
+
const batch = tokenizeBatch(['exp021_250101_WT_1', 'exp021_250101_WT_2', 'exp021_250101_Het_1', 'exp021_250101_Het_2'])
|
|
100
|
+
expect(batch.rows.map(row => row.length)).toEqual([2, 2, 2, 2])
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should count no shared prefix when the names have no experiment header', () => {
|
|
104
|
+
expect(tokenizeBatch(['QC_01', 'QC_02', 'QC_03']).sharedPrefix).toBe(0)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('should reject a date that is not in the calendar when the token has six digits', () => {
|
|
108
|
+
expect(parseDateToken('250231')).toBeUndefined()
|
|
109
|
+
})
|
|
110
|
+
})
|
|
@@ -346,8 +346,9 @@ describe('useAutoGroup auto-disable degenerate columns', () => {
|
|
|
346
346
|
].join('\n')
|
|
347
347
|
auto.parseInput()
|
|
348
348
|
|
|
349
|
+
// Replicate columns are visible fields too, but they are off by default.
|
|
349
350
|
const goodCols = auto.fields.value.filter(
|
|
350
|
-
f => f.cardinality > 1 && f.cardinality < auto.samples.value.length,
|
|
351
|
+
f => f.role !== 'replicate' && f.cardinality > 1 && f.cardinality < auto.samples.value.length,
|
|
351
352
|
)
|
|
352
353
|
for (const col of goodCols) {
|
|
353
354
|
expect(auto.enabledFields.value.has(col.index)).toBe(true)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
exp021_20250305_AK_blank_001
|
|
2
|
+
exp021_20250305_AK_blank_005
|
|
3
|
+
exp021_20250305_AK_blank_009
|
|
4
|
+
exp021_20250305_AK_blank_013
|
|
5
|
+
exp021_20250305_AK_blank_017
|
|
6
|
+
exp021_20250305_AK_blank_021
|
|
7
|
+
exp021_20250305_AK_blank_025
|
|
8
|
+
exp021_20250305_AK_blank_029
|
|
9
|
+
exp021_20250305_AK_blank_033
|
|
10
|
+
exp021_20250305_AK_blank_037
|
|
11
|
+
exp021_20250305_AK_blank_041
|
|
12
|
+
exp021_20250305_AK_blank_045
|
|
13
|
+
exp021_20250305_AK_blank_049
|
|
14
|
+
exp021_20250305_AK_blank_053
|
|
15
|
+
exp021_20250305_AK_blank_057
|
|
16
|
+
exp021_20250305_AK_blank_061
|
|
17
|
+
exp021_20250305_AK_blank_065
|
|
18
|
+
exp021_20250305_AK_blank_066
|
|
19
|
+
exp021_20250305_AK_ksEx5_inf2_B13_1_0min_022
|
|
20
|
+
exp021_20250305_AK_ksEx5_inf2_B13_1_120min_052
|
|
21
|
+
exp021_20250305_AK_ksEx5_inf2_B13_1_20min_042
|
|
22
|
+
exp021_20250305_AK_ksEx5_inf2_B13_1_240min_015
|
|
23
|
+
exp021_20250305_AK_ksEx5_inf2_B13_1_360min_031
|
|
24
|
+
exp021_20250305_AK_ksEx5_inf2_B13_1_40min_059
|
|
25
|
+
exp021_20250305_AK_ksEx5_inf2_B13_2_0min_050
|
|
26
|
+
exp021_20250305_AK_ksEx5_inf2_B13_2_120min_016
|
|
27
|
+
exp021_20250305_AK_ksEx5_inf2_B13_2_20min_006
|
|
28
|
+
exp021_20250305_AK_ksEx5_inf2_B13_2_240min_035
|
|
29
|
+
exp021_20250305_AK_ksEx5_inf2_B13_2_360min_038
|
|
30
|
+
exp021_20250305_AK_ksEx5_inf2_B13_2_40min_055
|
|
31
|
+
exp021_20250305_AK_ksEx5_inf2_B13_6_0min_036
|
|
32
|
+
exp021_20250305_AK_ksEx5_inf2_B13_6_120min_002
|
|
33
|
+
exp021_20250305_AK_ksEx5_inf2_B13_6_20min_034
|
|
34
|
+
exp021_20250305_AK_ksEx5_inf2_B13_6_240min_039
|
|
35
|
+
exp021_20250305_AK_ksEx5_inf2_B13_6_360min_010
|
|
36
|
+
exp021_20250305_AK_ksEx5_inf2_B13_6_40min_043
|
|
37
|
+
exp021_20250305_AK_ksEx5_inf2_B13_7_0min_062
|
|
38
|
+
exp021_20250305_AK_ksEx5_inf2_B13_7_120min_028
|
|
39
|
+
exp021_20250305_AK_ksEx5_inf2_B13_7_20min_058
|
|
40
|
+
exp021_20250305_AK_ksEx5_inf2_B13_7_240min_018
|
|
41
|
+
exp021_20250305_AK_ksEx5_inf2_B13_7_360min_026
|
|
42
|
+
exp021_20250305_AK_ksEx5_inf2_B13_7_40min_060
|
|
43
|
+
exp021_20250305_AK_ksEx5_inf3_B13_1_0min_019
|
|
44
|
+
exp021_20250305_AK_ksEx5_inf3_B13_1_120min_032
|
|
45
|
+
exp021_20250305_AK_ksEx5_inf3_B13_1_20min_064
|
|
46
|
+
exp021_20250305_AK_ksEx5_inf3_B13_1_240min_024
|
|
47
|
+
exp021_20250305_AK_ksEx5_inf3_B13_1_360min_014
|
|
48
|
+
exp021_20250305_AK_ksEx5_inf3_B13_1_40min_040
|
|
49
|
+
exp021_20250305_AK_ksEx5_inf3_B13_2_0min_063
|
|
50
|
+
exp021_20250305_AK_ksEx5_inf3_B13_2_120min_012
|
|
51
|
+
exp021_20250305_AK_ksEx5_inf3_B13_2_20min_047
|
|
52
|
+
exp021_20250305_AK_ksEx5_inf3_B13_2_240min_007
|
|
53
|
+
exp021_20250305_AK_ksEx5_inf3_B13_2_360min_023
|
|
54
|
+
exp021_20250305_AK_ksEx5_inf3_B13_2_40min_003
|
|
55
|
+
exp021_20250305_AK_ksEx5_inf3_B13_6_0min_004
|
|
56
|
+
exp021_20250305_AK_ksEx5_inf3_B13_6_120min_044
|
|
57
|
+
exp021_20250305_AK_ksEx5_inf3_B13_6_20min_030
|
|
58
|
+
exp021_20250305_AK_ksEx5_inf3_B13_6_240min_048
|
|
59
|
+
exp021_20250305_AK_ksEx5_inf3_B13_6_360min_011
|
|
60
|
+
exp021_20250305_AK_ksEx5_inf3_B13_6_40min_054
|
|
61
|
+
exp021_20250305_AK_ksEx5_inf3_B13_7_0min_046
|
|
62
|
+
exp021_20250305_AK_ksEx5_inf3_B13_7_120min_020
|
|
63
|
+
exp021_20250305_AK_ksEx5_inf3_B13_7_20min_056
|
|
64
|
+
exp021_20250305_AK_ksEx5_inf3_B13_7_240min_008
|
|
65
|
+
exp021_20250305_AK_ksEx5_inf3_B13_7_360min_051
|
|
66
|
+
exp021_20250305_AK_ksEx5_inf3_B13_7_40min_027
|
|
67
|
+
ksEx5_inf2_B13_1_120min
|
|
68
|
+
ksEx5_inf2_S13_8_120min
|
|
69
|
+
ksEx5_inf2_S13_8_120min__MS1_AGC100_70-1000_02
|
|
70
|
+
ksEx5_inf2_S13_8_120min_MS1_AGC100_70-500_03
|
|
71
|
+
ksEx5_inf2_S13_8_120min_MS1_AGC100_70-500_shape_g_04
|
|
72
|
+
ksEx5_inf2_S13_8_120min_MS1_AGC100_70-500_stead_g_05
|
|
73
|
+
ksEx7a_Betaine_10nM_std_in_442
|
|
74
|
+
ksEx7a_Betaine_Valine_Mix_10nM_std_in_442
|
|
75
|
+
ksEx7a_Blank_442
|
|
76
|
+
ksEx7a_FullLabBetaine_10nM_std_in_442
|
|
77
|
+
ksEx7a_FullLabBetaine_Valine_Mix_10nM_std_in_442
|
|
78
|
+
ksEx7a_Valine_10nM_std_in_442
|
|
79
|
+
ksEx7b_Betaine_10nM_std_in_442_MS1_AGC100_70-1000_02
|
|
80
|
+
ksEx7b_Betaine_10nM_std_in_442_MS1_AGC100_70-500_03
|
|
81
|
+
ksEx7b_Betaine_10nM_std_in_442_MS1_AGC100_70-500_shape_g_04
|
|
82
|
+
ksEx7b_Betaine_10nM_std_in_442_MS1_AGC100_70-500_stead_g_05
|
|
83
|
+
ksEx7b_Betaine_Valine_Mix_10nM_std_in_442__MS1_AGC100_70-1000_02
|
|
84
|
+
ksEx7b_Betaine_Valine_Mix_10nM_std_in_442_MS1_AGC100_70-500_03
|
|
85
|
+
ksEx7b_Betaine_Valine_Mix_10nM_std_in_442_MS1_AGC100_70-500_shape_g_04
|
|
86
|
+
ksEx7b_Betaine_Valine_Mix_10nM_std_in_442_MS1_AGC100_70-500_stead_g_05
|
|
87
|
+
ksEx7b_Blank_442_02
|
|
88
|
+
ksEx7b_Blank_442_03
|
|
89
|
+
ksEx7b_Blank_442_04
|
|
90
|
+
ksEx7b_Blank_442_05
|
|
91
|
+
ksEx7b_Blank_442_06
|
|
92
|
+
ksEx7b_FullLabBetaine_10nM_std_in_442__MS1_AGC100_70-1000_02
|
|
93
|
+
ksEx7b_FullLabBetaine_10nM_std_in_442_MS1_AGC100_70-500_03
|
|
94
|
+
ksEx7b_FullLabBetaine_10nM_std_in_442_MS1_AGC100_70-500_shape_g_04
|
|
95
|
+
ksEx7b_FullLabBetaine_10nM_std_in_442_MS1_AGC100_70-500_stead_g_05
|
|
96
|
+
ksEx7b_FullLabBetaine_Valine_Mix_10nM_std_in_442__MS1_AGC100_70-1000_02
|
|
97
|
+
ksEx7b_FullLabBetaine_Valine_Mix_10nM_std_in_442_MS1_AGC100_70-500_03
|
|
98
|
+
ksEx7b_FullLabBetaine_Valine_Mix_10nM_std_in_442_MS1_AGC100_70-500_shape_g_04
|
|
99
|
+
ksEx7b_FullLabBetaine_Valine_Mix_10nM_std_in_442_MS1_AGC100_70-500_stead_g_05
|
|
100
|
+
ksEx7b_Valine_10nM_std_in_442__MS1_AGC100_70-1000_02
|
|
101
|
+
ksEx7b_Valine_10nM_std_in_442_MS1_AGC100_70-500_03
|
|
102
|
+
ksEx7b_Valine_10nM_std_in_442_MS1_AGC100_70-500_shape_g_04
|
|
103
|
+
ksEx7b_Valine_10nM_std_in_442_MS1_AGC100_70-500_stead_g_05
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
exp031_250318_SU_105_media_DMSO_72_A_23
|
|
2
|
+
exp031_250318_SU_105_media_DMSO_72_B_17
|
|
3
|
+
exp031_250318_SU_105_media_DMSO_72_C_33
|
|
4
|
+
exp031_250318_SU_105_media_DMSO_72_D_02
|
|
5
|
+
exp031_250318_SU_105_media_TFP_72_A_20
|
|
6
|
+
exp031_250318_SU_105_media_TFP_72_B_34
|
|
7
|
+
exp031_250318_SU_105_media_TFP_72_C_30
|
|
8
|
+
exp031_250318_SU_105_media_TFP_72_D_18
|
|
9
|
+
exp031_250318_SU_270_media_DMSO_72_A_35
|
|
10
|
+
exp031_250318_SU_270_media_DMSO_72_B_11
|
|
11
|
+
exp031_250318_SU_270_media_DMSO_72_C_05
|
|
12
|
+
exp031_250318_SU_270_media_DMSO_72_D_37
|
|
13
|
+
exp031_250318_SU_270_media_TFP_72_A_09
|
|
14
|
+
exp031_250318_SU_270_media_TFP_72_B_31
|
|
15
|
+
exp031_250318_SU_270_media_TFP_72_C_13
|
|
16
|
+
exp031_250318_SU_270_media_TFP_72_D_21
|
|
17
|
+
exp031_250318_SU_blank_01
|
|
18
|
+
exp031_250318_SU_blank_06
|
|
19
|
+
exp031_250318_SU_blank_10
|
|
20
|
+
exp031_250318_SU_blank_12
|
|
21
|
+
exp031_250318_SU_blank_24
|
|
22
|
+
exp031_250318_SU_blank_32
|
|
23
|
+
exp031_250318_SU_blank_39
|
|
24
|
+
exp031_250318_SU_SF8628_media_DMSO_72_A_16
|
|
25
|
+
exp031_250318_SU_SF8628_media_DMSO_72_B_03
|
|
26
|
+
exp031_250318_SU_SF8628_media_DMSO_72_C_14
|
|
27
|
+
exp031_250318_SU_SF8628_media_DMSO_72_D_19
|
|
28
|
+
exp031_250318_SU_SF8628_media_TFP_72_A_28
|
|
29
|
+
exp031_250318_SU_SF8628_media_TFP_72_B_38
|
|
30
|
+
exp031_250318_SU_SF8628_media_TFP_72_C_08
|
|
31
|
+
exp031_250318_SU_SF8628_media_TFP_72_D_36
|
|
32
|
+
exp031_250318_SU_SU-VI_media_DMSO_72_A_26
|
|
33
|
+
exp031_250318_SU_SU-VI_media_DMSO_72_B_27
|
|
34
|
+
exp031_250318_SU_SU-VI_media_DMSO_72_C_29
|
|
35
|
+
exp031_250318_SU_SU-VI_media_DMSO_72_D_15
|
|
36
|
+
exp031_250318_SU_SU-VI_media_TFP_72_A_22
|
|
37
|
+
exp031_250318_SU_SU-VI_media_TFP_72_B_07
|
|
38
|
+
exp031_250318_SU_SU-VI_media_TFP_72_C_25
|
|
39
|
+
exp031_250318_SU_SU-VI_media_TFP_72_D_04
|