query.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. /**
  2. * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. */
  6. import * as path from 'path';
  7. import { Column } from '../../../mol-data/db';
  8. import { CifWriter } from '../../../mol-io/writer/cif';
  9. import { Structure, StructureQuery, StructureSelection, Model } from '../../../mol-model/structure';
  10. import { encode_mmCIF_categories } from '../../../mol-model/structure/export/mmcif';
  11. import { Progress } from '../../../mol-task';
  12. import { ConsoleLogger } from '../../../mol-util/console-logger';
  13. import { now } from '../../../mol-util/now';
  14. import { PerformanceMonitor } from '../../../mol-util/performance-monitor';
  15. import { ModelServerConfig as Config } from '../config';
  16. import { createModelPropertiesProviderFromConfig, ModelPropertiesProvider } from '../property-provider';
  17. import Version from '../version';
  18. import { Job, JobEntry } from './jobs';
  19. import { createStructureWrapperFromJobEntry, resolveStructures, StructureWrapper } from './structure-wrapper';
  20. import CifField = CifWriter.Field
  21. import { splitCamelCase } from '../../../mol-util/string';
  22. import { Encoder } from '../../../mol-io/writer/cif/encoder';
  23. import { Encoding } from './api';
  24. import { ComponentBond } from '../../../mol-model-formats/structure/property/bonds/chem_comp';
  25. import { SdfWriter } from '../../../mol-io/writer/sdf';
  26. import { MolWriter } from '../../../mol-io/writer/mol';
  27. import { Mol2Writer } from '../../../mol-io/writer/mol2';
  28. import { MolEncoder } from '../../../mol-io/writer/mol/encoder';
  29. import { Mol2Encoder } from '../../../mol-io/writer/mol2/encoder';
  30. import { ComponentAtom } from '../../../mol-model-formats/structure/property/atoms/chem_comp';
  31. import { Mat4 } from '../../../mol-math/linear-algebra';
  32. export interface Stats {
  33. structure: StructureWrapper,
  34. queryTimeMs: number,
  35. encodeTimeMs: number,
  36. resultSize: number
  37. }
  38. const perf = new PerformanceMonitor();
  39. let _propertyProvider: ModelPropertiesProvider;
  40. function propertyProvider() {
  41. if (_propertyProvider) return _propertyProvider;
  42. _propertyProvider = createModelPropertiesProviderFromConfig() || (() => []);
  43. return _propertyProvider;
  44. }
  45. export async function resolveJob(job: Job) {
  46. if (job.responseFormat.tarball) {
  47. return resolveMultiFile(job);
  48. } else {
  49. return resolveSingleFile(job);
  50. }
  51. }
  52. const SharedParams = {
  53. encoderName: `ModelServer ${Version}`
  54. };
  55. const SharedLigandWritingParams = {
  56. ...SharedParams,
  57. hydrogens: true
  58. };
  59. function createEncoder(job: Job): Encoder {
  60. switch (job.responseFormat.encoding) {
  61. case 'bcif':
  62. return CifWriter.createEncoder({
  63. ...SharedParams,
  64. binary: true,
  65. binaryAutoClassifyEncoding: true
  66. });
  67. case 'sdf':
  68. ensureCompatibleQueryType(job);
  69. return SdfWriter.createEncoder({
  70. ...SharedLigandWritingParams
  71. });
  72. case 'mol':
  73. ensureCompatibleQueryType(job);
  74. return MolWriter.createEncoder({
  75. ...SharedLigandWritingParams
  76. });
  77. case 'mol2':
  78. ensureCompatibleQueryType(job);
  79. return Mol2Writer.createEncoder({
  80. ...SharedLigandWritingParams
  81. });
  82. default:
  83. return CifWriter.createEncoder({
  84. ...SharedParams,
  85. binary: false,
  86. binaryAutoClassifyEncoding: true
  87. });
  88. }
  89. }
  90. function ensureCompatibleQueryType(job: Job) {
  91. job.entries.forEach(e => {
  92. if (e.queryDefinition.niceName !== 'Ligand') {
  93. throw Error("sdf, mol and mol2 encoding are only available for queries of type 'Ligand'");
  94. }
  95. });
  96. }
  97. async function resolveSingleFile(job: Job) {
  98. ConsoleLogger.logId(job.id, 'Query', `Starting (format: ${job.responseFormat.encoding}).`);
  99. const encoder = createEncoder(job);
  100. const headerMap = new Map<string, number>();
  101. for (const entry of job.entries) {
  102. let hasDataBlock = false;
  103. try {
  104. const structure = await createStructureWrapperFromJobEntry(entry, propertyProvider());
  105. let header = structure.cifFrame.header.toUpperCase();
  106. if (headerMap.has(header)) {
  107. const i = headerMap.get(header)! + 1;
  108. headerMap.set(header, i);
  109. header += ' ' + i;
  110. } else {
  111. headerMap.set(header, 0);
  112. }
  113. encoder.startDataBlock(header);
  114. hasDataBlock = true;
  115. await resolveJobEntry(entry, structure, encoder);
  116. } catch (e) {
  117. if (job.entries.length === 1) {
  118. throw e;
  119. } else {
  120. if (!hasDataBlock) {
  121. createErrorDataBlock(entry, encoder);
  122. }
  123. doError(entry, encoder, e);
  124. ConsoleLogger.errorId(entry.job.id, '' + e);
  125. }
  126. }
  127. }
  128. ConsoleLogger.logId(job.id, 'Query', 'Encoding.');
  129. encoder.encode();
  130. encoder.writeTo(job.writer);
  131. }
  132. function getFilename(i: number, entry: JobEntry, header: string, encoding: Encoding) {
  133. return `${i}_${header.toLowerCase()}_${splitCamelCase(entry.queryDefinition.name.replace(/\s/g, '_'), '-').toLowerCase()}.${encoding}`;
  134. }
  135. async function resolveMultiFile(job: Job) {
  136. ConsoleLogger.logId(job.id, 'Query', 'Starting.');
  137. let i = 0;
  138. for (const entry of job.entries) {
  139. const encoder = createEncoder(job);
  140. let hasDataBlock = false;
  141. let header = '';
  142. try {
  143. const structure = await createStructureWrapperFromJobEntry(entry, propertyProvider());
  144. header = structure.cifFrame.header;
  145. encoder.startDataBlock(structure.cifFrame.header);
  146. hasDataBlock = true;
  147. await resolveJobEntry(entry, structure, encoder);
  148. } catch(e) {
  149. if (!hasDataBlock) {
  150. header = createErrorDataBlock(entry, encoder);
  151. }
  152. ConsoleLogger.errorId(entry.job.id, '' + e);
  153. doError(entry, encoder, e);
  154. }
  155. ConsoleLogger.logId(job.id, 'Query', `Encoding ${entry.key}/${entry.queryDefinition.name}`);
  156. encoder.encode();
  157. job.writer.beginEntry(getFilename(++i, entry, header, job.responseFormat.encoding), encoder.getSize());
  158. encoder.writeTo(job.writer);
  159. job.writer.endEntry();
  160. ConsoleLogger.logId(job.id, 'Query', `Written ${entry.key}/${entry.queryDefinition.name}`);
  161. // await fileEntry;
  162. }
  163. }
  164. function createErrorDataBlock(job: JobEntry, encoder: CifWriter.Encoder<any>) {
  165. let header;
  166. if (job.sourceId === '_local_') header = path.basename(job.entryId).replace(/[^a-z0-9\-]/gi, '').toUpperCase();
  167. else header = job.entryId.replace(/[^a-z0-9\-]/gi, '').toUpperCase();
  168. encoder.startDataBlock(header);
  169. return header;
  170. }
  171. async function resolveJobEntry(entry: JobEntry, structure: StructureWrapper, encoder: CifWriter.Encoder<any>) {
  172. ConsoleLogger.logId(entry.job.id, 'Query', `Start ${entry.key}/${entry.queryDefinition.name}.`);
  173. try {
  174. perf.start('query');
  175. const sourceStructures = await resolveStructures(structure, entry.modelNums);
  176. if (!sourceStructures.length) throw new Error('Model not available');
  177. let structures: Structure[] = sourceStructures;
  178. if (entry.queryDefinition.structureTransform) {
  179. structures = [];
  180. for (const s of sourceStructures) {
  181. structures.push(await entry.queryDefinition.structureTransform(entry.normalizedParams, s));
  182. }
  183. }
  184. const modelNums = entry.modelNums || (structure.models as Model[]).map(m => m.modelNum);
  185. const queries = structures.map(s => entry.queryDefinition.query(entry.normalizedParams, s, modelNums));
  186. const result: Structure[] = [];
  187. for (let i = 0; i < structures.length; i++) {
  188. const s = StructureSelection.unionStructure(StructureQuery.run(queries[i], structures[i], { timeoutMs: Config.queryTimeoutMs }));
  189. if (s.elementCount > 0) {
  190. if (!entry.transform || Mat4.isIdentity(entry.transform)) {
  191. result.push(s);
  192. } else {
  193. result.push(Structure.transform(s, entry.transform));
  194. }
  195. }
  196. }
  197. perf.end('query');
  198. ConsoleLogger.logId(entry.job.id, 'Query', `Queried ${entry.key}/${entry.queryDefinition.name}.`);
  199. perf.start('encode');
  200. encoder.binaryEncodingProvider = getEncodingProvider(structure);
  201. // TODO: this actually needs to "reversible" in case of error.
  202. encoder.writeCategory(_model_server_result, entry);
  203. encoder.writeCategory(_model_server_params, entry);
  204. if (entry.queryDefinition.niceName === 'Ligand') {
  205. if (encoder instanceof MolEncoder) {
  206. encoder.setComponentAtomData(ComponentAtom.Provider.get(structure.models[0])!);
  207. }
  208. if (encoder instanceof MolEncoder || encoder instanceof Mol2Encoder) {
  209. encoder.setComponentBondData(ComponentBond.Provider.get(structure.models[0])!);
  210. }
  211. }
  212. // TODO propagate data for cif/bcif as well?
  213. if (!entry.copyAllCategories && entry.queryDefinition.filter) encoder.setFilter(entry.queryDefinition.filter);
  214. if (result.length > 0) encode_mmCIF_categories(encoder, result, { copyAllCategories: entry.copyAllCategories });
  215. if (!entry.copyAllCategories && entry.queryDefinition.filter) encoder.setFilter();
  216. perf.end('encode');
  217. const stats: Stats = {
  218. structure: structure,
  219. queryTimeMs: perf.time('query'),
  220. encodeTimeMs: perf.time('encode'),
  221. resultSize: result.reduce((n, s) => n + s.elementCount, 0)
  222. };
  223. encoder.writeCategory(_model_server_stats, stats);
  224. ConsoleLogger.logId(entry.job.id, 'Query', `Written ${entry.key}/${entry.queryDefinition.name}.`);
  225. return encoder;
  226. } catch (e) {
  227. ConsoleLogger.errorId(entry.job.id, e);
  228. doError(entry, encoder, e);
  229. } finally {
  230. encoder.binaryEncodingProvider = void 0;
  231. }
  232. }
  233. function getEncodingProvider(structure: StructureWrapper) {
  234. if (!structure.isBinary) return void 0;
  235. return CifWriter.createEncodingProviderFromCifFrame(structure.cifFrame);
  236. }
  237. function doError(entry: JobEntry, encoder: CifWriter.Encoder<any>, e: any) {
  238. encoder.writeCategory(_model_server_result, entry);
  239. encoder.writeCategory(_model_server_params, entry);
  240. encoder.writeCategory(_model_server_error, '' + e);
  241. }
  242. const maxTime = Config.queryTimeoutMs;
  243. export function abortingObserver(p: Progress) {
  244. if (now() - p.root.progress.startedTime > maxTime) {
  245. p.requestAbort(`Exceeded maximum allowed time for a query (${maxTime}ms)`);
  246. }
  247. }
  248. function string<T>(name: string, str: (data: T, i: number) => string, isSpecified?: (data: T) => boolean): CifField<number, T> {
  249. if (isSpecified) {
  250. return CifField.str(name, (i, d) => str(d, i), { valueKind: (i, d) => isSpecified(d) ? Column.ValueKind.Present : Column.ValueKind.NotPresent });
  251. }
  252. return CifField.str(name, (i, d) => str(d, i));
  253. }
  254. function int32<T>(name: string, value: (data: T) => number): CifField<number, T> {
  255. return CifField.int(name, (i, d) => value(d));
  256. }
  257. const _model_server_result_fields: CifField<any, JobEntry>[] = [
  258. string<JobEntry>('job_id', ctx => '' + ctx.job.id),
  259. string<JobEntry>('datetime_utc', ctx => ctx.job.datetime_utc),
  260. string<JobEntry>('server_version', ctx => Version),
  261. string<JobEntry>('query_name', ctx => ctx.queryDefinition.name),
  262. string<JobEntry>('source_id', ctx => ctx.sourceId),
  263. string<JobEntry>('entry_id', ctx => ctx.entryId),
  264. ];
  265. const _model_server_params_fields: CifField<number, string[]>[] = [
  266. string<string[]>('name', (ctx, i) => ctx[i][0]),
  267. string<string[]>('value', (ctx, i) => ctx[i][1])
  268. ];
  269. const _model_server_error_fields: CifField<number, string>[] = [
  270. string<string>('message', (ctx, i) => ctx)
  271. ];
  272. const _model_server_stats_fields: CifField<number, Stats>[] = [
  273. int32<Stats>('io_time_ms', ctx => ctx.structure.info.readTime | 0),
  274. int32<Stats>('parse_time_ms', ctx => ctx.structure.info.parseTime | 0),
  275. // int32<Stats>('attach_props_time_ms', ctx => ctx.structure.info.attachPropsTime | 0),
  276. int32<Stats>('create_model_time_ms', ctx => ctx.structure.info.createModelTime | 0),
  277. int32<Stats>('query_time_ms', ctx => ctx.queryTimeMs | 0),
  278. int32<Stats>('encode_time_ms', ctx => ctx.encodeTimeMs | 0),
  279. int32<Stats>('element_count', ctx => ctx.resultSize | 0),
  280. ];
  281. const _model_server_result: CifWriter.Category<JobEntry> = {
  282. name: 'model_server_result',
  283. instance: (job) => CifWriter.categoryInstance(_model_server_result_fields, { data: job, rowCount: 1 })
  284. };
  285. const _model_server_error: CifWriter.Category<string> = {
  286. name: 'model_server_error',
  287. instance: (message) => CifWriter.categoryInstance(_model_server_error_fields, { data: message, rowCount: 1 })
  288. };
  289. const _model_server_params: CifWriter.Category<JobEntry> = {
  290. name: 'model_server_params',
  291. instance(job) {
  292. const params: string[][] = [];
  293. for (const k of Object.keys(job.normalizedParams)) {
  294. params.push([k, JSON.stringify(job.normalizedParams[k])]);
  295. }
  296. return CifWriter.categoryInstance(_model_server_params_fields, { data: params, rowCount: params.length });
  297. }
  298. };
  299. const _model_server_stats: CifWriter.Category<Stats> = {
  300. name: 'model_server_stats',
  301. instance: (stats) => CifWriter.categoryInstance(_model_server_stats_fields, { data: stats, rowCount: 1 })
  302. };