diff --git a/src/mol-model/structure/query.ts b/src/mol-model/structure/query.ts index 775112fbc134d27d7e75f9de26952be6b58a4e80..afbf8577f199a7b9aa33a3fd88b49a29acdfdab0 100644 --- a/src/mol-model/structure/query.ts +++ b/src/mol-model/structure/query.ts @@ -9,11 +9,13 @@ import { StructureQuery } from './query/query' export * from './query/context' import * as generators from './query/queries/generators' import * as modifiers from './query/queries/modifiers' +import * as combinators from './query/queries/combinators' import pred from './query/predicates' export const Queries = { generators, modifiers, + combinators, pred } diff --git a/src/mol-model/structure/query/queries/combinators.ts b/src/mol-model/structure/query/queries/combinators.ts index 854c54726292a58380805bf3578da5e8fb8414fe..05adcf3983414635adceb1f139cf3198d16c1268 100644 --- a/src/mol-model/structure/query/queries/combinators.ts +++ b/src/mol-model/structure/query/queries/combinators.ts @@ -6,8 +6,14 @@ import { StructureQuery } from '../query'; import { StructureSelection } from '../selection'; +import { none } from './generators'; export function merge(queries: ArrayLike<StructureQuery>): StructureQuery { + if (queries.length === 0) { + return none; + } else if (queries.length === 1) { + return queries[0]; + } return ctx => { const ret = StructureSelection.UniqueBuilder(ctx.inputStructure); for (let i = 0; i < queries.length; i++) { diff --git a/src/mol-model/structure/query/queries/generators.ts b/src/mol-model/structure/query/queries/generators.ts index c7e7d770e41dd544afeb30d9485fb92383c5d56a..f8ba1c17f37084ff7518e817ca40e4451d6cde35 100644 --- a/src/mol-model/structure/query/queries/generators.ts +++ b/src/mol-model/structure/query/queries/generators.ts @@ -11,6 +11,7 @@ import { Segmentation } from 'mol-data/int' import { LinearGroupingBuilder } from '../utils/builders'; import { QueryPredicate, QueryFn, QueryContextView } from '../context'; +export const none: StructureQuery = ctx => StructureSelection.Sequence(ctx.inputStructure, []); export const all: StructureQuery = ctx => StructureSelection.Singletons(ctx.inputStructure, ctx.inputStructure); export interface AtomsQueryParams { diff --git a/src/servers/model/query/atoms.ts b/src/servers/model/query/atoms.ts new file mode 100644 index 0000000000000000000000000000000000000000..3aca8010867dc61a2d6392bd7dec5e0cfdcee22e --- /dev/null +++ b/src/servers/model/query/atoms.ts @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { QueryPredicate, StructureElement, StructureProperties as Props, Queries } from 'mol-model/structure'; +import { AtomsQueryParams } from 'mol-model/structure/query/queries/generators'; + +export function getAtomsTests(params: any): Partial<AtomsQueryParams>[] { + if (Array.isArray(params)) { + return params.map(p => atomsTest(p)); + } else { + return [atomsTest(params)]; + } +} + +function atomsTest(params: any): Partial<AtomsQueryParams> { + return { + entityTest: entityTest(params), + chainTest: chainTest(params), + residueTest: residueTest(params), + atomTest: atomTest(params) + }; +} + +function entityTest(params: any): QueryPredicate | undefined { + if (typeof params.entity_id === 'undefined') return void 0; + const p = Props.entity.id, id = '' + params.label_entity_id; + return ctx => p(ctx.element) === id; +} + +function chainTest(params: any): QueryPredicate | undefined { + if (typeof params.label_asym_id !== 'undefined') { + const p = Props.chain.label_asym_id, id = '' + params.label_asym_id; + return ctx => p(ctx.element) === id; + } + if (typeof params.auth_asym_id !== 'undefined') { + const p = Props.chain.auth_asym_id, id = '' + params.auth_asym_id; + return ctx => p(ctx.element) === id; + } + return void 0; +} + +function residueTest(params: any): QueryPredicate | undefined { + const props: StructureElement.Property<any>[] = [], values: any[] = []; + + if (typeof params.label_seq_id !== 'undefined') { + props.push(Props.residue.label_seq_id); + values.push(+params.label_seq_id); + } + + if (typeof params.auth_seq_id !== 'undefined') { + props.push(Props.residue.auth_seq_id); + values.push(+params.auth_seq_id); + } + + if (typeof params.label_comp_id !== 'undefined') { + props.push(Props.residue.label_comp_id); + values.push(params.label_comp_id); + } + + if (typeof params.auth_comp_id !== 'undefined') { + props.push(Props.residue.auth_comp_id); + values.push(params.auth_comp_id); + } + + if (typeof params.pdbx_PDB_ins_code !== 'undefined') { + props.push(Props.residue.pdbx_PDB_ins_code); + values.push(params.pdbx_PDB_ins_code); + } + + return andEqual(props, values); +} + +function atomTest(params: any): QueryPredicate | undefined { + const props: StructureElement.Property<any>[] = [], values: any[] = []; + + if (typeof params.label_atom_id !== 'undefined') { + props.push(Props.atom.label_atom_id); + values.push(+params.label_atom_id); + } + + if (typeof params.auth_atom_id !== 'undefined') { + props.push(Props.atom.auth_atom_id); + values.push(+params.auth_atom_id); + } + + if (typeof params.type_symbol !== 'undefined') { + props.push(Props.atom.type_symbol); + values.push(+params.type_symbol); + } + + return andEqual(props, values); +} + +function andEqual(props: StructureElement.Property<any>[], values: any[]): QueryPredicate | undefined { + switch (props.length) { + case 0: return void 0; + case 1: return ctx => props[0](ctx.element) === values[0]; + case 2: return ctx => props[0](ctx.element) === values[0] && props[1](ctx.element) === values[1]; + case 3: return ctx => props[0](ctx.element) === values[0] && props[1](ctx.element) === values[1] && props[2](ctx.element) === values[2]; + default: { + const len = props.length; + return ctx => { + for (let i = 0; i < len; i++) if (!props[i](ctx.element) !== values[i]) return false; + return true; + }; + } + } +} \ No newline at end of file diff --git a/src/servers/model/server/api.ts b/src/servers/model/server/api.ts index 95879cce0d577d835c825e8c12f643b6cc4878e5..d05ae9dea9714780dd6be5225898381821b3214e 100644 --- a/src/servers/model/server/api.ts +++ b/src/servers/model/server/api.ts @@ -4,9 +4,11 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { StructureQuery, Queries, Structure, StructureElement, StructureSymmetry, StructureProperties as Props, QueryPredicate } from 'mol-model/structure'; +import { Queries, Structure, StructureQuery, StructureSymmetry } from 'mol-model/structure'; +import { getAtomsTests } from '../query/atoms'; export enum QueryParamType { + JSON, String, Integer, Float @@ -18,7 +20,7 @@ export interface QueryParamInfo { description?: string, required?: boolean, defaultValue?: any, - exampleValue?: string, + exampleValues?: string[], validation?: (v: any) => void } @@ -32,7 +34,7 @@ export interface QueryDefinition { structureTransform?: (params: any, s: Structure) => Promise<Structure> } -const AtomSiteParameters = { +const AtomSiteParams = { entity_id: <QueryParamInfo>{ name: 'entity_id', type: QueryParamType.String, description: 'Corresponds to the \'_entity.id\' or \'*.label_entity_id\' field, depending on the context.' }, label_asym_id: <QueryParamInfo>{ name: 'label_asym_id', type: QueryParamType.String, description: 'Corresponds to the \'_atom_site.label_asym_id\' field.' }, @@ -45,110 +47,76 @@ const AtomSiteParameters = { pdbx_PDB_ins_code: <QueryParamInfo>{ name: 'pdbx_PDB_ins_code', type: QueryParamType.String, description: 'Corresponds to the \'_atom_site.pdbx_PDB_ins_code\' field.' }, }; -// function entityTest(params: any): Element.Predicate | undefined { -// if (typeof params.entity_id === 'undefined') return void 0; -// const p = Props.entity.id, id = '' + params.entityId; -// return Element.property(l => p(l) === id); -// } - -function entityTest1_555(params: any): QueryPredicate | undefined { - if (typeof params.entity_id === 'undefined') return ctx => ctx.element.unit.conformation.operator.isIdentity; - const p = Props.entity.id, id = '' + params.entityId; - return ctx => ctx.element.unit.conformation.operator.isIdentity && p(ctx.element) === id; -} - -function chainTest(params: any): QueryPredicate | undefined { - if (typeof params.label_asym_id !== 'undefined') { - const p = Props.chain.label_asym_id, id = '' + params.label_asym_id; - return ctx => p(ctx.element) === id; - } - if (typeof params.auth_asym_id !== 'undefined') { - const p = Props.chain.auth_asym_id, id = '' + params.auth_asym_id; - return ctx => p(ctx.element) === id; - } - return void 0; -} - -function residueTest(params: any): QueryPredicate | undefined { - const props: StructureElement.Property<any>[] = [], values: any[] = []; - - if (typeof params.label_seq_id !== 'undefined') { - props.push(Props.residue.label_seq_id); - values.push(+params.label_seq_id); - } - - if (typeof params.auth_seq_id !== 'undefined') { - props.push(Props.residue.auth_seq_id); - values.push(+params.auth_seq_id); - } - - if (typeof params.label_comp_id !== 'undefined') { - props.push(Props.residue.label_comp_id); - values.push(params.label_comp_id); - } - - if (typeof params.auth_comp_id !== 'undefined') { - props.push(Props.residue.auth_comp_id); - values.push(params.auth_comp_id); - } - - if (typeof params.pdbx_PDB_ins_code !== 'undefined') { - props.push(Props.residue.pdbx_PDB_ins_code); - values.push(params.pdbx_PDB_ins_code); - } +const AtomSiteTestParams: QueryParamInfo = { + name: 'atom_site', + type: QueryParamType.JSON, + description: 'Object or array of objects describing atom properties. Name are same as in wwPDB mmCIF dictionary of the atom_site category.', + exampleValues: [`{ label_comp_id: 'ALA' }`, `{ label_seq_id: 123, label_asym_id: 'A' }`] +}; - switch (props.length) { - case 0: return void 0; - case 1: return ctx => props[0](ctx.element) === values[0]; - case 2: return ctx => props[0](ctx.element) === values[0] && props[1](ctx.element) === values[1]; - case 3: return ctx => props[0](ctx.element) === values[0] && props[1](ctx.element) === values[1] && props[2](ctx.element) === values[2]; - default: { - const len = props.length; - return ctx => { - for (let i = 0; i < len; i++) if (!props[i](ctx.element) !== values[i]) return false; - return true; - }; +const RadiusParam: QueryParamInfo = { + name: 'radius', + type: QueryParamType.Float, + defaultValue: 5, + exampleValues: ['5'], + description: 'Value in Angstroms.', + validation(v: any) { + if (v < 1 || v > 10) { + throw `Invalid radius for residue interaction query (must be a value between 1 and 10).`; } } -} - -// function buildResiduesQuery(params: any): Query.Provider { -// return Queries.generators.atoms({ entityTest: entityTest(params), chainTest: chainTest(params), residueTest: residueTest(params) }); -// } +}; const QueryMap: { [id: string]: Partial<QueryDefinition> } = { 'full': { niceName: 'Full Structure', query: () => Queries.generators.all, description: 'The full structure.' }, + 'atoms': { + niceName: 'Atoms', + description: 'Atoms satisfying the given criteria.', + query: p => Queries.combinators.merge(getAtomsTests(p.atom_site).map(test => Queries.generators.atoms(test))), + params: [ AtomSiteTestParams ] + }, + 'symmetryMates': { + niceName: 'Symmetry Mates', + description: 'Computes crystal symmetry mates within the specified radius', + query: () => Queries.generators.all, + structureTransform(p, s) { + return StructureSymmetry.builderSymmetryMates(s, p.radius).run(); + }, + }, + 'assembly': { + niceName: 'Assembly', + description: 'Computes crystal symmetry mates within the specified radius', + query: () => Queries.generators.all, + structureTransform(p, s) { + return StructureSymmetry.builderSymmetryMates(s, p.radius).run(); + }, + }, 'residueInteraction': { niceName: 'Residues Inside a Sphere', - description: 'Identifies all residues within the given radius from the source residue.', + description: 'Identifies all residues within the given radius from the source residue. Takes crystal symmetry into account.', query(p) { - const center = Queries.generators.atoms({ entityTest: entityTest1_555(p), chainTest: chainTest(p), residueTest: residueTest(p) }); + const tests = getAtomsTests(p); + const center = Queries.combinators.merge(tests.map(test => Queries.generators.atoms({ + ...test, + entityTest: test.entityTest + ? ctx => test.entityTest!(ctx) && ctx.element.unit.conformation.operator.isIdentity + : ctx => ctx.element.unit.conformation.operator.isIdentity + }))); return Queries.modifiers.includeSurroundings(center, { radius: p.radius, wholeResidues: true }); }, structureTransform(p, s) { return StructureSymmetry.builderSymmetryMates(s, p.radius).run(); }, params: [ - AtomSiteParameters.entity_id, - AtomSiteParameters.label_asym_id, - AtomSiteParameters.auth_asym_id, - AtomSiteParameters.label_comp_id, - AtomSiteParameters.auth_comp_id, - AtomSiteParameters.pdbx_PDB_ins_code, - AtomSiteParameters.label_seq_id, - AtomSiteParameters.auth_seq_id, - { - name: 'radius', - type: QueryParamType.Float, - defaultValue: 5, - exampleValue: '5', - description: 'Value in Angstroms.', - validation(v: any) { - if (v < 1 || v > 10) { - throw `Invalid radius for residue interaction query (must be a value between 1 and 10).`; - } - } - }, + AtomSiteParams.entity_id, + AtomSiteParams.label_asym_id, + AtomSiteParams.auth_asym_id, + AtomSiteParams.label_comp_id, + AtomSiteParams.auth_comp_id, + AtomSiteParams.pdbx_PDB_ins_code, + AtomSiteParams.label_seq_id, + AtomSiteParams.auth_seq_id, + RadiusParam, ] }, };