@datagrok/sequence-translator 1.3.14 → 1.4.0
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/CHANGELOG.md +23 -1
- package/dist/package-test.js +1 -1
- package/dist/package-test.js.map +1 -1
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- package/files/polytool-rules/rules_example.json +34 -34
- package/files/samples/cyclized.csv +6 -0
- package/package.json +9 -10
- package/src/apps/common/view/components/colored-input/colored-text-input.ts +2 -2
- package/src/apps/pattern/view/components/bulk-convert/column-input.ts +2 -2
- package/src/apps/pattern/view/components/bulk-convert/table-input.ts +8 -6
- package/src/apps/pattern/view/components/edit-block-controls.ts +5 -5
- package/src/apps/pattern/view/components/load-block-controls.ts +7 -3
- package/src/apps/pattern/view/components/numeric-label-visibility-controls.ts +1 -1
- package/src/apps/pattern/view/components/strand-editor/header-controls.ts +2 -2
- package/src/apps/pattern/view/components/strand-editor/strand-controls.ts +2 -2
- package/src/apps/pattern/view/components/terminal-modification-editor.ts +1 -1
- package/src/apps/structure/view/ui.ts +5 -5
- package/src/apps/translator/view/ui.ts +27 -18
- package/src/package-test.ts +1 -0
- package/src/package.ts +34 -12
- package/src/polytool/pt-conversion.ts +2 -33
- package/src/polytool/pt-convert-editor.ts +116 -0
- package/src/polytool/pt-dialog.ts +177 -97
- package/src/polytool/pt-enumeration-helm-dialog.ts +338 -282
- package/src/polytool/pt-enumeration-helm.ts +6 -2
- package/src/polytool/pt-placeholders-input.ts +1 -2
- package/src/polytool/utils.ts +0 -7
- package/src/tests/polytool-convert-tests.ts +99 -0
- package/src/tests/polytool-enumerate-tests.ts +21 -5
- package/src/utils/context-menu.ts +7 -10
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as grok from 'datagrok-api/grok';
|
|
2
|
+
import * as ui from 'datagrok-api/ui';
|
|
3
|
+
import * as DG from 'datagrok-api/dg';
|
|
4
|
+
import {_package} from '../package';
|
|
5
|
+
import {defaultErrorHandler} from '../utils/err-info';
|
|
6
|
+
import {PT_UI_DIALOG_CONVERSION, PT_UI_RULES_USED} from './const';
|
|
7
|
+
import {RuleInputs, RULES_PATH, RULES_STORAGE_NAME} from './pt-rules';
|
|
8
|
+
|
|
9
|
+
/** Inputs of polyToolConvert2 package function */
|
|
10
|
+
export enum P {
|
|
11
|
+
table = 'table',
|
|
12
|
+
seqCol = 'seqCol',
|
|
13
|
+
generateHelm = 'generateHelm',
|
|
14
|
+
chiralityEngine = 'chiralityEngine',
|
|
15
|
+
rules = 'rules'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type PolyToolConvertInputs = {
|
|
19
|
+
table: DG.InputBase<DG.DataFrame | null>;
|
|
20
|
+
seqCol: DG.InputBase<DG.Column | null>;
|
|
21
|
+
generateHelm: DG.InputBase<boolean>;
|
|
22
|
+
chiralityEngine: DG.InputBase<boolean>;
|
|
23
|
+
rules: { header: HTMLElement, form: HTMLDivElement };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class PolyToolConvertFuncEditor {
|
|
27
|
+
private inputs: PolyToolConvertInputs;
|
|
28
|
+
private readonly ruleInputs: RuleInputs;
|
|
29
|
+
|
|
30
|
+
protected constructor(
|
|
31
|
+
private readonly call: DG.FuncCall,
|
|
32
|
+
) {
|
|
33
|
+
this.ruleInputs = new RuleInputs(RULES_PATH, RULES_STORAGE_NAME, '.json');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private async initInputs(): Promise<void> {
|
|
37
|
+
const getParam = (pName: string) => this.call.inputParams[pName];
|
|
38
|
+
|
|
39
|
+
this.inputs = {
|
|
40
|
+
table: (() => {
|
|
41
|
+
const p = getParam(P.table);
|
|
42
|
+
return ui.input.table(p.property.caption, {value: p.value});
|
|
43
|
+
})(),
|
|
44
|
+
seqCol: (() => {
|
|
45
|
+
const p = getParam(P.seqCol);
|
|
46
|
+
return ui.input.column(p.property.caption, {value: p.value, table: p.value.dataFrame});
|
|
47
|
+
})(),
|
|
48
|
+
generateHelm: ui.input.forProperty(getParam(P.generateHelm).property),
|
|
49
|
+
chiralityEngine: ui.input.forProperty(getParam(P.chiralityEngine).property),
|
|
50
|
+
rules: {
|
|
51
|
+
header: ui.inlineText([PT_UI_RULES_USED]),
|
|
52
|
+
form: await this.ruleInputs.getForm(),
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static async create(call: DG.FuncCall): Promise<PolyToolConvertFuncEditor> {
|
|
58
|
+
const editor = new PolyToolConvertFuncEditor(call);
|
|
59
|
+
await editor.initInputs();
|
|
60
|
+
return editor;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// -- Params --
|
|
64
|
+
|
|
65
|
+
private async getParams(): Promise<{ [p: string]: any }> {
|
|
66
|
+
return {
|
|
67
|
+
table: this.inputs.table.value!,
|
|
68
|
+
seqCol: this.inputs.seqCol.value!,
|
|
69
|
+
generateHelm: this.inputs.generateHelm.value,
|
|
70
|
+
chiralityEngine: this.inputs.chiralityEngine.value,
|
|
71
|
+
rules: await this.ruleInputs.getActive(),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public async showDialog(): Promise<DG.Column<string>> {
|
|
76
|
+
const formDiv = ui.div([
|
|
77
|
+
this.inputs.table,
|
|
78
|
+
this.inputs.seqCol,
|
|
79
|
+
this.inputs.generateHelm,
|
|
80
|
+
this.inputs.chiralityEngine,
|
|
81
|
+
this.inputs.rules.header,
|
|
82
|
+
this.inputs.rules.form,
|
|
83
|
+
],
|
|
84
|
+
{style: {minWidth: '320px'}});
|
|
85
|
+
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
ui.dialog({title: PT_UI_DIALOG_CONVERSION})
|
|
88
|
+
.add(formDiv)
|
|
89
|
+
.onOK(async () => {
|
|
90
|
+
const callParams = await this.getParams();
|
|
91
|
+
const res = (await this.call.func.prepare(callParams).call(true)).getOutputParamValue() as DG.Column<string>;
|
|
92
|
+
resolve(res);
|
|
93
|
+
})
|
|
94
|
+
.onCancel(() => { reject(new Error('Cancelled by user')); })
|
|
95
|
+
.show();
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// -- UI --
|
|
100
|
+
|
|
101
|
+
public widget(): DG.Widget {
|
|
102
|
+
throw new Error('not implemented');
|
|
103
|
+
|
|
104
|
+
// const inputsForm = ui.inputs(Object.entries(this.inputs)
|
|
105
|
+
// .filter(([inputName, _input]) => !['table', 'sequence'].includes(inputName))
|
|
106
|
+
// .map(([_inputName, input]) => input));
|
|
107
|
+
// const doBtn = ui.button('Convert', () => {
|
|
108
|
+
// (async () => {
|
|
109
|
+
// const callParams = this.getParams();
|
|
110
|
+
// await this.call.func.prepare(callParams).call(true);
|
|
111
|
+
// })()
|
|
112
|
+
// .catch((err: any) => { defaultErrorHandler(err, true); });
|
|
113
|
+
// });
|
|
114
|
+
// return DG.Widget.fromRoot(ui.divV([inputsForm, ui.div(doBtn)]));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -4,16 +4,19 @@ import * as ui from 'datagrok-api/ui';
|
|
|
4
4
|
import * as DG from 'datagrok-api/dg';
|
|
5
5
|
|
|
6
6
|
import $ from 'cash-dom';
|
|
7
|
+
import {Unsubscribable} from 'rxjs';
|
|
7
8
|
|
|
8
9
|
import {getHelmHelper} from '@datagrok-libraries/bio/src/helm/helm-helper';
|
|
10
|
+
import {errInfo} from '@datagrok-libraries/bio/src/utils/err-info';
|
|
11
|
+
import {NOTATION} from '@datagrok-libraries/bio/src/utils/macromolecule';
|
|
12
|
+
import {getSeqHelper, ISeqHelper} from '@datagrok-libraries/bio/src/utils/seq-helper';
|
|
9
13
|
|
|
10
|
-
import {RuleInputs, RULES_PATH, RULES_STORAGE_NAME} from './pt-rules';
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
import {handleError} from './utils';
|
|
14
|
+
import {getRules, RuleInputs, RULES_PATH, RULES_STORAGE_NAME} from './pt-rules';
|
|
15
|
+
import {doPolyToolConvert} from './pt-conversion';
|
|
14
16
|
import {defaultErrorHandler} from '../utils/err-info';
|
|
15
17
|
import {getLibrariesList} from './utils';
|
|
16
18
|
import {getEnumerationChem, PT_CHEM_EXAMPLE} from './pt-enumeration-chem';
|
|
19
|
+
|
|
17
20
|
import {
|
|
18
21
|
PT_ERROR_DATAFRAME, PT_UI_ADD_HELM, PT_UI_DIALOG_CONVERSION, PT_UI_DIALOG_ENUMERATION,
|
|
19
22
|
PT_UI_GET_HELM, PT_UI_RULES_USED, PT_UI_USE_CHIRALITY, PT_WARNING_COLUMN
|
|
@@ -31,100 +34,126 @@ export function polyToolEnumerateChemUI(cell?: DG.Cell): void {
|
|
|
31
34
|
});
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
export async function
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
targetColumnInput,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
.
|
|
65
|
-
.onOK(async () => {
|
|
66
|
-
const pi = DG.TaskBarProgressIndicator.create('PolyTool converting');
|
|
67
|
-
try {
|
|
68
|
-
const sequencesCol = targetColumnInput.value;
|
|
69
|
-
if (!sequencesCol) {
|
|
70
|
-
grok.shell.warning(PT_WARNING_COLUMN);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
37
|
+
export async function polyToolConvertUI(): Promise<void> {
|
|
38
|
+
let dialog: DG.Dialog;
|
|
39
|
+
try {
|
|
40
|
+
dialog = await getPolyToolConvertDialog();
|
|
41
|
+
dialog.show();
|
|
42
|
+
} catch (err: any) {
|
|
43
|
+
const [errMsg, errStack] = errInfo(err);
|
|
44
|
+
grok.shell.warning('To run PolyTool Conversion, open a dataframe with macromolecules');
|
|
45
|
+
_package.logger.error(errMsg, undefined, errStack);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function getPolyToolConvertDialog(targetCol?: DG.Column): Promise<DG.Dialog> {
|
|
50
|
+
const subs: Unsubscribable[] = [];
|
|
51
|
+
const destroy = () => {
|
|
52
|
+
for (const sub of subs) sub.unsubscribe();
|
|
53
|
+
};
|
|
54
|
+
try {
|
|
55
|
+
const targetColumns = grok.shell.t.columns.bySemTypeAll(DG.SEMTYPE.MACROMOLECULE);
|
|
56
|
+
if (!targetColumns)
|
|
57
|
+
throw new Error(PT_ERROR_DATAFRAME);
|
|
58
|
+
|
|
59
|
+
const targetColumnInput = ui.input.column('Column', {
|
|
60
|
+
table: grok.shell.t, value: targetColumns[0],
|
|
61
|
+
filter: (col: DG.Column) => col.semType === DG.SEMTYPE.MACROMOLECULE
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
targetColumnInput.value = targetCol ? targetCol : targetColumnInput.value;
|
|
65
|
+
|
|
66
|
+
const generateHelmChoiceInput = ui.input.bool(PT_UI_GET_HELM, {value: true});
|
|
67
|
+
ui.tooltip.bind(generateHelmChoiceInput.root, PT_UI_ADD_HELM);
|
|
73
68
|
|
|
74
|
-
|
|
69
|
+
const chiralityEngineInput = ui.input.bool(PT_UI_USE_CHIRALITY, {value: false});
|
|
70
|
+
const ruleInputs = new RuleInputs(RULES_PATH, RULES_STORAGE_NAME, '.json');
|
|
71
|
+
const rulesHeader = ui.inlineText([PT_UI_RULES_USED]);
|
|
72
|
+
ui.tooltip.bind(rulesHeader, 'Add or specify rules to use');
|
|
73
|
+
const rulesForm = await ruleInputs.getForm();
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
const div = ui.div([
|
|
76
|
+
targetColumnInput,
|
|
77
|
+
generateHelmChoiceInput,
|
|
78
|
+
chiralityEngineInput,
|
|
79
|
+
rulesHeader,
|
|
80
|
+
rulesForm
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
const exec = async (): Promise<void> => {
|
|
85
|
+
try {
|
|
86
|
+
const ruleFileList = await ruleInputs.getActive();
|
|
87
|
+
await polyToolConvert(targetColumnInput.value!, generateHelmChoiceInput.value!, chiralityEngineInput.value!, ruleFileList);
|
|
80
88
|
} catch (err: any) {
|
|
81
|
-
|
|
82
|
-
} finally {
|
|
83
|
-
pi.close();
|
|
89
|
+
defaultErrorHandler(err);
|
|
84
90
|
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const dialog = ui.dialog(PT_UI_DIALOG_CONVERSION)
|
|
94
|
+
.add(div)
|
|
95
|
+
.onOK(() => { exec(); });
|
|
96
|
+
subs.push(dialog.onClose.subscribe(() => {
|
|
97
|
+
destroy();
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
return dialog;
|
|
101
|
+
} catch (err: any) {
|
|
102
|
+
destroy(); // on failing to build a dialog
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
88
105
|
}
|
|
89
106
|
|
|
90
107
|
async function getPolyToolEnumerationChemDialog(cell?: DG.Cell): Promise<DG.Dialog> {
|
|
91
|
-
const [
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
molInput.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
108
|
+
const subs: Unsubscribable[] = [];
|
|
109
|
+
const destroy = () => {
|
|
110
|
+
for (const sub of subs) sub.unsubscribe();
|
|
111
|
+
};
|
|
112
|
+
try {
|
|
113
|
+
|
|
114
|
+
const [libList, helmHelper] = await Promise.all([
|
|
115
|
+
getLibrariesList(), getHelmHelper()]);
|
|
116
|
+
|
|
117
|
+
const molStr = (cell && cell.rowIndex >= 0) ? cell.value : PT_CHEM_EXAMPLE;//cell ? cell.value : PT_CHEM_EXAMPLE;
|
|
118
|
+
let molfileValue: string = await (async (): Promise<string> => {
|
|
119
|
+
if (DG.chem.isMolBlock(molStr)) return molStr;
|
|
120
|
+
return (await grok.functions.call('Chem:convertMolNotation', {
|
|
121
|
+
molecule: molStr,
|
|
122
|
+
sourceNotation: cell?.column.getTag(DG.TAGS.UNITS) ?? DG.chem.Notation.Unknown,
|
|
123
|
+
targetNotation: DG.chem.Notation.MolBlock,
|
|
124
|
+
}));
|
|
125
|
+
})();
|
|
126
|
+
|
|
127
|
+
const molInput = new DG.chem.Sketcher(DG.chem.SKETCHER_MODE.EXTERNAL);
|
|
128
|
+
molInput.syncCurrentObject = false;
|
|
129
|
+
// sketcher.setMolFile(col.tags[ALIGN_BY_SCAFFOLD_TAG]);
|
|
130
|
+
molInput.onChanged.subscribe((_: any) => {
|
|
131
|
+
molfileValue = molInput.getMolFile();
|
|
132
|
+
});
|
|
133
|
+
molInput.root.classList.add('ui-input-editor');
|
|
134
|
+
molInput.root.style.marginTop = '3px';
|
|
135
|
+
molInput.setMolFile(molfileValue);
|
|
136
|
+
|
|
137
|
+
//const helmInput = helmHelper.createHelmInput('Macromolecule', {value: helmValue});
|
|
138
|
+
const screenLibrary = ui.input.choice('Library to use', {value: null, items: libList});
|
|
139
|
+
|
|
140
|
+
molInput.root.setAttribute('style', `min-width:250px!important;`);
|
|
141
|
+
molInput.root.setAttribute('style', `max-width:250px!important;`);
|
|
142
|
+
screenLibrary.input.setAttribute('style', `min-width:250px!important;`);
|
|
143
|
+
|
|
144
|
+
const div = ui.div([
|
|
145
|
+
molInput.root,
|
|
146
|
+
screenLibrary.root
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
subs.push(grok.events.onCurrentCellChanged.subscribe(() => {
|
|
150
|
+
const cell = grok.shell.tv.dataFrame.currentCell;
|
|
151
|
+
|
|
152
|
+
if (cell.column.semType === DG.SEMTYPE.MOLECULE)
|
|
153
|
+
molInput.setValue(cell.value);
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
const exec = async (): Promise<void> => {
|
|
128
157
|
try {
|
|
129
158
|
const molString = molInput.getMolFile();
|
|
130
159
|
|
|
@@ -140,12 +169,63 @@ async function getPolyToolEnumerationChemDialog(cell?: DG.Cell): Promise<DG.Dial
|
|
|
140
169
|
}
|
|
141
170
|
} catch (err: any) {
|
|
142
171
|
defaultErrorHandler(err);
|
|
143
|
-
} finally {
|
|
144
|
-
cccSubs.unsubscribe();
|
|
145
172
|
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Displays the molecule from a current cell (monitors changes)
|
|
176
|
+
const dialog = ui.dialog(PT_UI_DIALOG_ENUMERATION)
|
|
177
|
+
.add(div)
|
|
178
|
+
.onOK(() => {
|
|
179
|
+
exec().finally(() => { destroy(); });
|
|
180
|
+
})
|
|
181
|
+
.onCancel(() => {
|
|
182
|
+
destroy();
|
|
183
|
+
});
|
|
184
|
+
subs.push(dialog.onClose.subscribe(() => {
|
|
185
|
+
destroy();
|
|
186
|
+
}));
|
|
187
|
+
return dialog;
|
|
188
|
+
} catch (err: any) {
|
|
189
|
+
destroy();
|
|
190
|
+
throw err;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
149
193
|
|
|
150
|
-
|
|
194
|
+
/** Returns Helm and molfile columns. */
|
|
195
|
+
export async function polyToolConvert(
|
|
196
|
+
seqCol: DG.Column<string>, generateHelm: boolean, chiralityEngine: boolean, ruleFiles: string[]
|
|
197
|
+
): Promise<[DG.Column, DG.Column]> {
|
|
198
|
+
const pi = DG.TaskBarProgressIndicator.create('PolyTool converting...');
|
|
199
|
+
try {
|
|
200
|
+
const getUnusedName = (df: DG.DataFrame | undefined, colName: string): string => {
|
|
201
|
+
if (!df) return colName;
|
|
202
|
+
return df.columns.getUnusedName(colName);
|
|
203
|
+
};
|
|
204
|
+
await getHelmHelper(); // initializes JSDraw and org
|
|
205
|
+
|
|
206
|
+
const table = seqCol.dataFrame;
|
|
207
|
+
const rules = await getRules(ruleFiles);
|
|
208
|
+
const resList = doPolyToolConvert(seqCol.toList(), rules);
|
|
209
|
+
|
|
210
|
+
const resHelmColName = getUnusedName(table, `transformed(${seqCol.name})`);
|
|
211
|
+
const resHelmCol = DG.Column.fromType(DG.COLUMN_TYPE.STRING, resHelmColName, resList.length)
|
|
212
|
+
.init((rowIdx: number) => { return resList[rowIdx];});
|
|
213
|
+
resHelmCol.semType = DG.SEMTYPE.MACROMOLECULE;
|
|
214
|
+
resHelmCol.meta.units = NOTATION.HELM;
|
|
215
|
+
resHelmCol.setTag(DG.TAGS.CELL_RENDERER, 'helm');
|
|
216
|
+
if (generateHelm && table) table.columns.add(resHelmCol, true);
|
|
217
|
+
|
|
218
|
+
const seqHelper: ISeqHelper = await getSeqHelper();
|
|
219
|
+
const toAtomicLevelRes = await seqHelper.helmToAtomicLevel(resHelmCol, chiralityEngine, /* highlight */ generateHelm);
|
|
220
|
+
const resMolCol = toAtomicLevelRes.molCol;
|
|
221
|
+
resMolCol.name = getUnusedName(table, `molfile(${seqCol.name})`);
|
|
222
|
+
resMolCol.semType = DG.SEMTYPE.MOLECULE;
|
|
223
|
+
if (table) {
|
|
224
|
+
table.columns.add(resMolCol, true);
|
|
225
|
+
await grok.data.detectSemanticTypes(table);
|
|
226
|
+
}
|
|
227
|
+
return [resHelmCol, resMolCol];
|
|
228
|
+
} finally {
|
|
229
|
+
pi.close();
|
|
230
|
+
}
|
|
151
231
|
}
|