Browse Source

mmcif export updates- rename chains with asm/symm/ncs operators- molstar_atom_site_operator_mapping category

David Sehnal 5 years ago
parent
commit
5575c61577

+ 1 - 1
src/apps/viewer/extensions/cellpack/model.ts

@@ -111,7 +111,7 @@ function getAssembly(transforms: Mat4[], structure: Structure) {
 
     for (let i = 0, il = transforms.length; i < il; ++i) {
         const id = `${i + 1}`
-        const op = SymmetryOperator.create(id, transforms[i], { assembly: { id, operIndex: i, operList: [ id ] } })
+        const op = SymmetryOperator.create(id, transforms[i], { assembly: { id, operId: i, operList: [ id ] } })
         for (const unit of units) {
             builder.addWithOperator(unit, op)
         }

+ 2 - 2
src/mol-io/writer/cif.ts

@@ -29,8 +29,8 @@ export namespace CifWriter {
         return binary ? new BinaryEncoder(encoderName, params ? params.binaryEncodingPovider : void 0, params ? !!params.binaryAutoClassifyEncoding : false) : new TextEncoder();
     }
 
-    export function fields<K = number, D = any>() {
-        return Field.build<K, D>();
+    export function fields<K = number, D = any, N extends string = string>() {
+        return Field.build<K, D, N>();
     }
 
     import E = Encoding

+ 20 - 8
src/mol-io/writer/cif/encoder.ts

@@ -72,25 +72,32 @@ export namespace Field {
         return int(name, (e, d, i) => i + 1, { typedArray: Int32Array, encoder: ArrayEncoding.by(ArrayEncoding.delta).and(ArrayEncoding.runLength).and(ArrayEncoding.integerPacking) })
     }
 
-    export class Builder<K = number, D = any> {
+    export class Builder<K = number, D = any, N extends string = string> {
         private fields: Field<K, D>[] = [];
 
-        index(name: string) {
+        index(name: N) {
             this.fields.push(Field.index(name));
             return this;
         }
 
-        str(name: string, value: (k: K, d: D, index: number) => string, params?: ParamsBase<K, D>) {
+        str(name: N, value: (k: K, d: D, index: number) => string, params?: ParamsBase<K, D>) {
             this.fields.push(Field.str(name, value, params));
             return this;
         }
 
-        int(name: string, value: (k: K, d: D, index: number) => number, params?:  ParamsBase<K, D> & { typedArray?: ArrayEncoding.TypedArrayCtor }) {
+        int(name: N, value: (k: K, d: D, index: number) => number, params?:  ParamsBase<K, D> & { typedArray?: ArrayEncoding.TypedArrayCtor }) {
             this.fields.push(Field.int(name, value, params));
             return this;
         }
 
-        float(name: string, value: (k: K, d: D, index: number) => number, params?: ParamsBase<K, D> & { typedArray?: ArrayEncoding.TypedArrayCtor, digitCount?: number }) {
+        vec(name: N, values: ((k: K, d: D, index: number) => number)[], params?: ParamsBase<K, D> & { typedArray?: ArrayEncoding.TypedArrayCtor }) {
+            for (let i = 0; i < values.length; i++) {
+                this.fields.push(Field.int(`${name}[${i + 1}]`, values[i], params));
+            }
+            return this;
+        }
+
+        float(name: N, value: (k: K, d: D, index: number) => number, params?: ParamsBase<K, D> & { typedArray?: ArrayEncoding.TypedArrayCtor, digitCount?: number }) {
             this.fields.push(Field.float(name, value, params));
             return this;
         }
@@ -103,8 +110,8 @@ export namespace Field {
         getFields() { return this.fields; }
     }
 
-    export function build<K = number, D = any>() {
-        return new Builder<K, D>();
+    export function build<K = number, D = any, N extends string  = string>() {
+        return new Builder<K, D, N>();
     }
 }
 
@@ -215,14 +222,19 @@ export namespace Category {
 
 export interface Encoder<T = string | Uint8Array> extends EncoderBase {
     setFilter(filter?: Category.Filter): void,
+    isCategoryIncluded(name: string): boolean,
     setFormatter(formatter?: Category.Formatter): void,
 
     startDataBlock(header: string): void,
-    writeCategory<Ctx>(category: Category<Ctx>, context?: Ctx): void,
+    writeCategory<Ctx>(category: Category<Ctx>, context?: Ctx, options?: Encoder.WriteCategoryOptions): void,
     getData(): T
 }
 
 export namespace Encoder {
+    export interface WriteCategoryOptions {
+        ignoreFilter?: boolean
+    }
+
     export function writeDatabase(encoder: Encoder, name: string, database: Database<Database.Schema>) {
         encoder.startDataBlock(name);
         for (const table of database._tableNames) {

+ 6 - 2
src/mol-io/writer/cif/encoder/binary.ts

@@ -32,6 +32,10 @@ export default class BinaryEncoder implements Encoder<Uint8Array> {
         this.filter = filter || Category.DefaultFilter;
     }
 
+    isCategoryIncluded(name: string) {
+        return this.filter.includeCategory(name);
+    }
+
     setFormatter(formatter?: Category.Formatter) {
         this.formatter = formatter || Category.DefaultFormatter;
     }
@@ -43,7 +47,7 @@ export default class BinaryEncoder implements Encoder<Uint8Array> {
         });
     }
 
-    writeCategory<Ctx>(category: Category<Ctx>, context?: Ctx) {
+    writeCategory<Ctx>(category: Category<Ctx>, context?: Ctx, options?: Encoder.WriteCategoryOptions) {
         if (!this.data) {
             throw new Error('The writer contents have already been encoded, no more writing.');
         }
@@ -52,7 +56,7 @@ export default class BinaryEncoder implements Encoder<Uint8Array> {
             throw new Error('No data block created.');
         }
 
-        if (!this.filter.includeCategory(category.name)) return;
+        if (!options?.ignoreFilter && !this.filter.includeCategory(category.name)) return;
 
         const { instance, rowCount, source } = getCategoryInstanceData(category, context);
         if (!rowCount) return;

+ 6 - 2
src/mol-io/writer/cif/encoder/text.ts

@@ -23,6 +23,10 @@ export default class TextEncoder implements Encoder<string> {
         this.filter = filter || Category.DefaultFilter;
     }
 
+    isCategoryIncluded(name: string) {
+        return this.filter.includeCategory(name);
+    }
+
     setFormatter(formatter?: Category.Formatter) {
         this.formatter = formatter || Category.DefaultFormatter;
     }
@@ -32,7 +36,7 @@ export default class TextEncoder implements Encoder<string> {
         StringBuilder.write(this.builder, `data_${(header || '').replace(/[ \n\t]/g, '').toUpperCase()}\n#\n`);
     }
 
-    writeCategory<Ctx>(category: Category<Ctx>, context?: Ctx) {
+    writeCategory<Ctx>(category: Category<Ctx>, context?: Ctx, options?: Encoder.WriteCategoryOptions) {
         if (this.encoded) {
             throw new Error('The writer contents have already been encoded, no more writing.');
         }
@@ -41,7 +45,7 @@ export default class TextEncoder implements Encoder<string> {
             throw new Error('No data block created.');
         }
 
-        if (!this.filter.includeCategory(category.name)) return;
+        if (!options?.ignoreFilter && !this.filter.includeCategory(category.name)) return;
         const { instance, rowCount, source } = getCategoryInstanceData(category, context);
         if (!rowCount) return;
 

+ 7 - 9
src/mol-math/geometry/symmetry-operator.ts

@@ -16,8 +16,8 @@ interface SymmetryOperator {
         readonly id: string,
         /** pointers to `pdbx_struct_oper_list.id` or empty list */
         readonly operList: string[],
-        /** (arbitrary) index of the operator to be used in suffix */
-        readonly operIndex: number
+        /** (arbitrary) unique id of the operator to be used in suffix */
+        readonly operId: number
     },
 
     /** pointer to `struct_ncs_oper.id` or empty string */
@@ -35,8 +35,7 @@ interface SymmetryOperator {
 
     /**
      * Suffix based on operator type.
-     * - Empty if isIdentity
-     * - Assembly: _{assembly.operIndex + 1}
+     * - Assembly: _assembly.operId
      * - Crytal: -op_ijk
      * - ncs: _ncsId
      */
@@ -55,18 +54,17 @@ namespace SymmetryOperator {
         const _hkl = hkl ? Vec3.clone(hkl) : Vec3.zero();
         spgrOp = defaults(spgrOp, -1);
         ncsId = ncsId || '';
-        const isIdentity = Mat4.isIdentity(matrix);
-        const suffix = getSuffix(isIdentity, info);
+        const suffix = getSuffix(info);
         if (Mat4.isIdentity(matrix)) return { name, assembly, matrix, inverse: Mat4.identity(), isIdentity: true, hkl: _hkl, spgrOp, ncsId, suffix };
         if (!Mat4.isRotationAndTranslation(matrix, RotationTranslationEpsilon)) throw new Error(`Symmetry operator (${name}) must be a composition of rotation and translation.`);
         return { name, assembly, matrix, inverse: Mat4.invert(Mat4.zero(), matrix), isIdentity: false, hkl: _hkl, spgrOp, ncsId, suffix };
     }
 
-    function getSuffix(isIdentity: boolean, info?: CreateInfo) {
-        if (!info || isIdentity) return '';
+    function getSuffix(info?: CreateInfo) {
+        if (!info) return '';
 
         if (info.assembly) {
-            return `_${info.assembly.operIndex + 1}`;
+            return `_${info.assembly.operId}`;
         }
 
         if (typeof info.spgrOp !== 'undefined' && typeof info.hkl !== 'undefined' && info.spgrOp !== -1) {

+ 1 - 1
src/mol-model-formats/structure/property/assembly.ts

@@ -117,7 +117,7 @@ function getAssemblyOperators(matrices: Matrices, operatorNames: string[][], sta
             Mat4.mul(m, m, matrices.get(op[i])!);
         }
         index++
-        operators[operators.length] = SymmetryOperator.create(`ASM_${index}`, m, { assembly: { id: assemblyId, operIndex: index, operList: op } });
+        operators[operators.length] = SymmetryOperator.create(`ASM_${index}`, m, { assembly: { id: assemblyId, operId: index, operList: op } });
     }
 
     return operators;

+ 22 - 6
src/mol-model/structure/export/categories/atom_site.ts

@@ -12,6 +12,22 @@ import CifField = CifWriter.Field
 import CifCategory = CifWriter.Category
 import E = CifWriter.Encodings
 
+const _label_asym_id = P.chain.label_asym_id;
+function atom_site_label_asym_id(e: StructureElement.Location) {
+    const l = _label_asym_id(e);
+    const suffix = e.unit.conformation.operator.suffix;
+    if (!suffix) return l;
+    return l + suffix;
+}
+
+const _auth_asym_id = P.chain.auth_asym_id;
+function atom_site_auth_asym_id(e: StructureElement.Location) {
+    const l = _auth_asym_id(e);
+    const suffix = e.unit.conformation.operator.suffix;
+    if (!suffix) return l;
+    return l + suffix;
+}
+
 const atom_site_fields = CifWriter.fields<StructureElement.Location, Structure>()
     .str('group_PDB', P.residue.group_PDB)
     .index('id')
@@ -29,14 +45,14 @@ const atom_site_fields = CifWriter.fields<StructureElement.Location, Structure>(
     .str('label_alt_id', P.atom.label_alt_id)
     .str('pdbx_PDB_ins_code', P.residue.pdbx_PDB_ins_code)
 
-    .str('label_asym_id', P.chain.label_asym_id)
+    .str('label_asym_id', atom_site_label_asym_id)
     .str('label_entity_id', P.chain.label_entity_id)
 
     .float('Cartn_x', P.atom.x, { digitCount: 3, encoder: E.fixedPoint3 })
     .float('Cartn_y', P.atom.y, { digitCount: 3, encoder: E.fixedPoint3 })
     .float('Cartn_z', P.atom.z, { digitCount: 3, encoder: E.fixedPoint3 })
     .float('occupancy', P.atom.occupancy, { digitCount: 2, encoder: E.fixedPoint2 })
-    .int('pdbx_formal_charge', P.atom.pdbx_formal_charge, { 
+    .int('pdbx_formal_charge', P.atom.pdbx_formal_charge, {
         encoder: E.deltaRLE,
         valueKind: (k, d) =>  k.unit.model.atomicHierarchy.atoms.pdbx_formal_charge.valueKind(k.element)
     })
@@ -44,12 +60,12 @@ const atom_site_fields = CifWriter.fields<StructureElement.Location, Structure>(
     .str('auth_atom_id', P.atom.auth_atom_id)
     .str('auth_comp_id', P.residue.auth_comp_id)
     .int('auth_seq_id', P.residue.auth_seq_id, { encoder: E.deltaRLE })
-    .str('auth_asym_id', P.chain.auth_asym_id)
+    .str('auth_asym_id', atom_site_auth_asym_id)
 
     .int('pdbx_PDB_model_num', P.unit.model_num, { encoder: E.deltaRLE })
-    .str('operator_name', P.unit.operator_name, {
-        shouldInclude: structure => structure.units.some(u => !u.conformation.operator.isIdentity)
-    })
+    // .str('operator_name', P.unit.operator_name, {
+    //     shouldInclude: structure => structure.units.some(u => !u.conformation.operator.isIdentity)
+    // })
     .getFields();
 
 export const _atom_site: CifCategory<CifExportContext> = {

+ 104 - 0
src/mol-model/structure/export/categories/atom_site_operator_mapping.ts

@@ -0,0 +1,104 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { SymmetryOperator } from '../../../../mol-math/geometry';
+import { CifExportContext } from '../mmcif';
+import { StructureElement, StructureProperties as P } from '../../structure';
+import Unit from '../../structure/unit';
+import { Segmentation } from '../../../../mol-data/int';
+import { CifWriter } from '../../../../mol-io/writer/cif';
+import { Column } from '../../../../mol-data/db';
+
+export function atom_site_operator_mapping(encoder: CifWriter.Encoder, ctx: CifExportContext) {
+    const entries = getEntries(ctx);
+    if (entries.length === 0) return;
+    encoder.writeCategory(Category, entries, { ignoreFilter: true });
+}
+
+export const AtomSiteOperatorMappingSchema = {
+    molstar_atom_site_operator_mapping: {
+        label_asym_id: Column.Schema.Str(),
+        auth_asym_id: Column.Schema.Str(),
+        operator_name: Column.Schema.Str(),
+        suffix: Column.Schema.Str(),
+
+        // assembly
+        assembly_operator_id: Column.Schema.Str(),
+        assembly_operator_index: Column.Schema.Int(),
+
+        // symmetry
+        symmetry_operator_index: Column.Schema.Int(),
+        symmetry_hkl: Column.Schema.Vector(3),
+
+        // NCS
+        ncs_id: Column.Schema.Str(),
+    }
+}
+
+const asmValueKind = (i: number, xs: Entry[]) => typeof xs[i].operator.assembly === 'undefined' ? Column.ValueKind.NotPresent : Column.ValueKind.Present;
+const symmetryValueKind = (i: number, xs: Entry[]) => xs[i].operator.spgrOp === -1 ? Column.ValueKind.NotPresent : Column.ValueKind.Present;
+
+const Fields = CifWriter.fields<number, Entry[], keyof (typeof AtomSiteOperatorMappingSchema)['molstar_atom_site_operator_mapping']>()
+    .str('label_asym_id', (i, xs) => xs[i].label_asym_id)
+    .str('auth_asym_id', (i, xs) => xs[i].auth_asym_id)
+    .str('operator_name', (i, xs) => xs[i].operator.name)
+    .str('suffix', (i, xs) => xs[i].operator.suffix)
+    // assembly
+    // TODO: include oper list as well?
+    .str('assembly_operator_id', (i, xs) => xs[i].operator.assembly?.id || '', { valueKind: asmValueKind })
+    .int('assembly_operator_index', (i, xs) => xs[i].operator.assembly?.operId || 0, { valueKind: asmValueKind })
+    // symmetry
+    .int('symmetry_operator_index', (i, xs) => xs[i].operator.spgrOp, { valueKind: symmetryValueKind })
+    .vec('symmetry_hkl', [(i, xs) => xs[i].operator.hkl[0], (i, xs) => xs[i].operator.hkl[1], (i, xs) => xs[i].operator.hkl[2]], { valueKind: symmetryValueKind })
+    // NCS
+    .str('ncs_id', (i, xs) => xs[i].operator.ncsId || '', { valueKind: (i, xs) => !xs[i].operator.ncsId ? Column.ValueKind.NotPresent : Column.ValueKind.Present })
+    .getFields()
+
+const Category: CifWriter.Category<Entry[]> = {
+    name: 'molstar_atom_site_operator_mapping',
+    instance(entries: Entry[]) {
+        return { fields: Fields, source: [{ data: entries, rowCount: entries.length }] };
+    }
+}
+
+interface Entry {
+    label_asym_id: string,
+    auth_asym_id: string,
+    operator: SymmetryOperator
+}
+
+function getEntries(ctx: CifExportContext) {
+    const existing = new Set<string>();
+    const entries: Entry[] = [];
+
+    for (const s of ctx.structures) {
+        const l = StructureElement.Location.create(s);
+        for (const unit of s.units) {
+            const operator = unit.conformation.operator;
+            if (!operator.suffix || unit.kind !== Unit.Kind.Atomic) continue;
+
+            l.unit = unit;
+
+            const { elements } = unit;
+            const chainsIt = Segmentation.transientSegments(unit.model.atomicHierarchy.chainAtomSegments, elements);
+            while (chainsIt.hasNext) {
+                const chainSegment = chainsIt.move();
+                l.element = elements[chainSegment.start];
+
+                const label_asym_id = P.chain.label_asym_id(l);
+                const key = `${label_asym_id}${operator.suffix}`;
+
+                if (existing.has(key)) continue;
+                existing.add(key);
+
+                const auth_asym_id = P.chain.label_asym_id(l);
+                entries.push({ label_asym_id, auth_asym_id, operator });
+            }
+        }
+    }
+
+    return entries;
+}

+ 5 - 0
src/mol-model/structure/export/mmcif.ts

@@ -16,6 +16,7 @@ import { Model } from '../model';
 import { getUniqueEntityIndicesFromStructures, copy_mmCif_category } from './categories/utils';
 import { _struct_asym, _entity_poly, _entity_poly_seq } from './categories/sequence';
 import { CustomPropertyDescriptor } from '../common/custom-property';
+import { atom_site_operator_mapping } from './categories/atom_site_operator_mapping';
 
 export interface CifExportContext {
     structures: Structure[],
@@ -136,6 +137,10 @@ export function encode_mmCIF_categories(encoder: CifWriter.Encoder, structures:
         encoder.writeCategory(cat, ctx);
     }
 
+    if ((!_params.skipCategoryNames || !_params.skipCategoryNames.has('atom_site')) && encoder.isCategoryIncluded('atom_site')) {
+        atom_site_operator_mapping(encoder, ctx);
+    }
+
     for (const customProp of models[0].customProperties.all) {
         encodeCustomProp(customProp, ctx, encoder, _params);
     }

+ 1 - 1
src/mol-model/structure/structure/symmetry.ts

@@ -26,7 +26,7 @@ namespace StructureSymmetry {
             const assembly = Symmetry.findAssembly(models[0], asmName);
             if (!assembly) throw new Error(`Assembly '${asmName}' is not defined.`);
 
-            const coordinateSystem = SymmetryOperator.create(assembly.id, Mat4.identity(), { assembly: { id: assembly.id, operIndex: 0, operList: [] } })
+            const coordinateSystem = SymmetryOperator.create(assembly.id, Mat4.identity(), { assembly: { id: assembly.id, operId: 0, operList: [] } })
             const assembler = Structure.Builder({ coordinateSystem, label: structure.label });
 
             const queryCtx = new QueryContext(structure);

+ 12 - 12
src/servers/model/config.ts

@@ -90,8 +90,8 @@ const DefaultModelServerConfig = {
      * - filename [source, mapping]
      * - URI [source, mapping, format]
      *
-     * Mapping is provided 'source' and 'id' variables to interpolate. 
-     * 
+     * Mapping is provided 'source' and 'id' variables to interpolate.
+     *
      * /static query uses 'pdb-cif' and 'pdb-bcif' source names.
      */
     sourceMap: [
@@ -152,7 +152,7 @@ function addServerArgs(parser: argparse.ArgumentParser) {
     parser.addArgument([ '--defaultSource' ], {
         defaultValue: DefaultModelServerConfig.defaultSource,
         metavar: 'DEFAULT_SOURCE',
-        help: `modifies which 'sourceMap' source to use by default` 
+        help: `modifies which 'sourceMap' source to use by default`
     });
     parser.addArgument([ '--sourceMap' ], {
         nargs: 2,
@@ -182,7 +182,7 @@ function addServerArgs(parser: argparse.ArgumentParser) {
 export type ModelServerConfig = typeof DefaultModelServerConfig
 export const ModelServerConfig = { ...DefaultModelServerConfig }
 
-export const ModelServerConfigTemplate: ModelServerConfig = { 
+export const ModelServerConfigTemplate: ModelServerConfig = {
     ...DefaultModelServerConfig,
     defaultSource: 'pdb-bcif',
     sourceMap: [
@@ -202,12 +202,12 @@ interface ServerJsonConfig {
 }
 
 function addJsonConfigArgs(parser: argparse.ArgumentParser) {
-    parser.addArgument(['--cfg'], { 
+    parser.addArgument(['--cfg'], {
         help: [
             'JSON config file path',
             'If a property is not specified, cmd line param/OS variable/default value are used.'
         ].join('\n'),
-        required: false 
+        required: false
     });
     parser.addArgument(['--printCfg'], { help: 'Print current config for validation and exit.', required: false, nargs: 0 });
     parser.addArgument(['--cfgTemplate'], { help: 'Prints default JSON config template to be modified and exits.', required: false, nargs: 0 });
@@ -228,7 +228,7 @@ function validateConfigAndSetupSourceMap() {
     if (!ModelServerConfig.sourceMap || ModelServerConfig.sourceMap.length === 0) {
         throw new Error(`Please provide 'sourceMap' configuration. See [-h] for available options.`);
     }
-    
+
     mapSourceAndIdToFilename = new Function('source', 'id', [
         'switch (source.toLowerCase()) {',
         ...ModelServerConfig.sourceMap.map(([source, path, format]) => `case '${source.toLowerCase()}': return [\`${path}\`, '${format}'];`),
@@ -254,23 +254,23 @@ export function configureServer() {
         console.log(JSON.stringify(ModelServerConfigTemplate, null, 2));
         process.exit(0);
     }
-    
+
     try {
         setConfig(config) // sets the config for global use
-    
+
         if (config.cfg) {
             const cfg = JSON.parse(fs.readFileSync(config.cfg, 'utf8')) as ModelServerConfig;
             setConfig(cfg);
         }
-    
+
         if (config.printCfg !== null) {
             console.log(JSON.stringify(ModelServerConfig, null, 2));
             process.exit(0);
         }
-    
+
         validateConfigAndSetupSourceMap();
     } catch (e) {
         console.error('' + e);
         process.exit(1);
-    }    
+    }
 }

+ 4 - 4
src/servers/model/server/api-web.ts

@@ -120,12 +120,12 @@ function mapQuery(app: express.Express, queryName: string, queryDefinition: Quer
 }
 
 function serveStatic(req: express.Request, res: express.Response) {
-    const source = req.params.source === 'bcif' 
+    const source = req.params.source === 'bcif'
         ? 'pdb-bcif'
         : req.params.source === 'cif'
-        ? 'pdb-cif'
-        : req.params.source;
-    
+            ? 'pdb-cif'
+            : req.params.source;
+
     const id = req.params.id;
     const [fn, format] = mapSourceAndIdToFilename(source, id);
     const binary = format === 'bcif' || fn.indexOf('.bcif') > 0;

+ 1 - 1
src/servers/model/server/structure-wrapper.ts

@@ -151,7 +151,7 @@ function readOrFetch(jobId: string, key: string, sourceId: string | '_local_', e
     const uri = mapped[0].toLowerCase();
     if (uri.startsWith('http://') || uri.startsWith('https://') || uri.startsWith('ftp://')) {
         return fetchDataAndFrame(jobId, mapped[0], (mapped[1] || 'cif').toLowerCase() as any, key);
-    } 
+    }
 
     if (!fs.existsSync(mapped[0])) throw new Error(`Could not find source file for '${key}'.`);
     return readDataAndFrame(mapped[0], key);