Ver Fonte

basic ligand writing

JonStargaryen há 4 anos atrás
pai
commit
a423970b9c

+ 74 - 4
src/servers/model/ligand-writer/ligand-encoder.ts

@@ -4,11 +4,19 @@
  * @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
  */
 
-import { StringBuilder } from '../../../mol-util';
+import { StringBuilder, deepEqual } from '../../../mol-util';
 import Writer from '../../../mol-io/writer/writer';
-import { Encoder, Category } from '../../../mol-io/writer/cif/encoder';
+import { Encoder, Category, Field } from '../../../mol-io/writer/cif/encoder';
 import { ComponentBond } from '../../../mol-model-formats/structure/property/bonds/comp';
 
+export interface Atom {
+    id: string,
+    x: number,
+    y: number,
+    z: number,
+    type_symbol: string
+}
+
 export abstract class LigandExplorer implements Encoder<string> {
     protected builder: StringBuilder;
     protected componentData: ComponentBond;
@@ -38,6 +46,63 @@ export abstract class LigandExplorer implements Encoder<string> {
         return StringBuilder.getString(this.builder);
     }
 
+    protected getAtoms<Ctx>(instance: Category.Instance<Ctx>, source: any): Atom[] {
+        const sortedFields = this.getSortedFields(instance, ['Cartn_x', 'Cartn_y', 'Cartn_z', 'type_symbol']);
+        const label_atom_id = this.getField(instance, 'label_atom_id');
+
+        // all of this is used to ensure that only 1 residue is written
+        const auxiliaryFields = this.getSortedFields(instance, ['label_seq_id', 'label_asym_id', 'pdbx_PDB_ins_code', 'pdbx_PDB_model_num']);
+
+        return this.getAtomsInternal(source, sortedFields, label_atom_id, auxiliaryFields);
+    }
+
+    private getAtomsInternal(source: any, fields: Field<any, any>[], label_atom_id: Field<any, any>, auxiliaryFields: Field<any, any>[]): Atom[] {
+        const atoms: Atom[] = [];
+        let index = 0;
+        let id: (string | number)[] | undefined = void 0;
+
+        // is outer loop even needed?
+        l: for (let _c = 0; _c < source.length; _c++) {
+            const src = source[_c];
+            const data = src.data;
+
+            if (src.rowCount === 0) continue;
+
+            const it = src.keys();
+            while (it.hasNext)  {
+                const key = it.move();
+
+                // ensure only a single residue is written
+                // TODO fairly certain this can be done at query level
+                if (!id) {
+                    id = auxiliaryFields.map(af => af.value(key, data, 0));
+                } else {
+                    if (!deepEqual(id, auxiliaryFields.map(af => af.value(key, data, index)))) {
+                        break l;
+                    }
+                }
+
+                const lai = label_atom_id.value(key, data, index) as string;
+                const label = this.getLabel(lai);
+                if (this.skipHydrogen(label)) {
+                    index++;
+                    continue;
+                }
+                const d: (string | number)[] = [lai];
+
+                for (let _f = 0, _fl = fields.length; _f < _fl; _f++) {
+                    const f: Field<any, any> = fields[_f]!;
+                    d.push(f.value(key, data, index));
+                }
+
+                atoms.push({ id: d[0] as string, x: d[1] as number, y: d[2] as number, z: d[3] as number, type_symbol: d[4] as string});
+                index++;
+            }
+        }
+
+        return atoms;
+    }
+
     protected skipHydrogen(label: string) {
         if (this.hydrogens) {
             return false;
@@ -49,14 +114,19 @@ export abstract class LigandExplorer implements Encoder<string> {
         return s.replace(/[^A-Z]+/g, '');
     }
 
-    protected getSortedFields<Ctx>(instance: Category.Instance<Ctx>, names: string[]) {
+    private getSortedFields<Ctx>(instance: Category.Instance<Ctx>, names: string[]) {
         return names.map(n => this.getField(instance, n));
     }
 
-    protected getField<Ctx>(instance: Category.Instance<Ctx>, name: string) {
+    private getField<Ctx>(instance: Category.Instance<Ctx>, name: string) {
         return instance.fields.find(f => f.name === name)!;
     }
 
+    protected getName<Ctx>(instance: Category.Instance<Ctx>, source: any): string {
+        const label_comp_id = this.getField(instance, 'label_comp_id');
+        return label_comp_id.value(source[0].keys().move(), source[0].data, 0) as string;
+    }
+
     startDataBlock() {}
 
     setFilter() {}

+ 1 - 1
src/servers/model/ligand-writer/mol.ts

@@ -15,7 +15,7 @@ export namespace MolWriter {
     }
 
     export function createEncoder(params?: EncoderParams): Encoder {
-        const { encoderName = 'mol*', hydrogens = false } = params || {};
+        const { encoderName = 'mol*', hydrogens = true } = params || {};
         return new MolEncoder(encoderName, false, hydrogens);
     }
 }

+ 18 - 79
src/servers/model/ligand-writer/mol/encoder.ts

@@ -4,8 +4,8 @@
  * @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
  */
 
-import { StringBuilder, deepEqual } from '../../../../mol-util';
-import { Category, Field } from '../../../../mol-io/writer/cif/encoder';
+import { StringBuilder } from '../../../../mol-util';
+import { Category } from '../../../../mol-io/writer/cif/encoder';
 import { getCategoryInstanceData } from '../../../../mol-io/writer/cif/encoder/util';
 import { LigandExplorer } from '../ligand-encoder';
 
@@ -42,32 +42,32 @@ export class MolEncoder extends LigandExplorer {
         // use separate builder because we still need to write Counts and Bonds line
         const ctab = StringBuilder.create();
         const bonds = StringBuilder.create();
-
         // write Atom block and gather data for Bonds and Charges
         const { instance, source } = getCategoryInstanceData(category, context);
-        const sortedFields = this.getSortedFields(instance, ['Cartn_x', 'Cartn_y', 'Cartn_z', 'type_symbol']);
-        const label_comp_id = this.getField(instance, 'label_comp_id');
-        const label_atom_id = this.getField(instance, 'label_atom_id');
-
-        // all of this is used to ensure that only 1 residue is written
-        // TODO potentially we could use a dedicated query mode or use the 'correct' SDF definition and allow an arbitrary number of molecules in a file
-        const auxiliaryFields = this.getSortedFields(instance, ['label_seq_id', 'label_asym_id', 'pdbx_PDB_ins_code', 'pdbx_PDB_model_num']);
 
         // write header
-        const name = label_comp_id.value(source[0].keys().move(), source[0].data, 0) as string;
+        const name = this.getName(instance, source);
         // 3rd lines must be present and can contain comments
-        StringBuilder.write(this.builder, `${name}\n  ${this.encoder}\n\n`);
+        StringBuilder.writeSafe(this.builder, `${name}\n  ${this.encoder}\n\n`);
 
         const bondMap = this.componentData.entries.get(name)!;
         let bondCount = 0;
 
         // traverse once to determine all actually present atoms
-        const atoms = this.getAtoms(source, sortedFields, label_atom_id, auxiliaryFields, ctab);
+        const atoms = this.getAtoms(instance, source);
         for (let i1 = 0, il = atoms.length; i1 < il; i1++) {
-            bondMap.map.get(atoms[i1])!.forEach((v, k) => {
-                const i2 = atoms.indexOf(k);
+            const atom = atoms[i1];
+            StringBuilder.writePadLeft(ctab, atom.x.toFixed(4), 10);
+            StringBuilder.writePadLeft(ctab, atom.y.toFixed(4), 10);
+            StringBuilder.writePadLeft(ctab, atom.z.toFixed(4), 10);
+            StringBuilder.whitespace1(ctab);
+            StringBuilder.writePadRight(ctab, atom.type_symbol, 2);
+            StringBuilder.writeSafe(ctab, '  0  0  0  0  0  0  0  0  0  0  0  0\n');
+
+            bondMap.map.get(atom.id)!.forEach((v, k) => {
+                const i2 = atoms.findIndex(e => e.id === k);
                 const label2 = this.getLabel(k);
-                if (i1 < i2 && atoms.indexOf(k) > -1 && !this.skipHydrogen(label2)) {
+                if (i1 < i2 && atoms.findIndex(e => e.id === k) > -1 && !this.skipHydrogen(label2)) {
                     const { order } = v;
                     StringBuilder.writeIntegerPadLeft(bonds, i1 + 1, 3);
                     StringBuilder.writeIntegerPadLeft(bonds, i2 + 1, 3);
@@ -81,7 +81,7 @@ export class MolEncoder extends LigandExplorer {
         // write counts line
         StringBuilder.writeIntegerPadLeft(this.builder, atoms.length, 3);
         StringBuilder.writeIntegerPadLeft(this.builder, bondCount, 3);
-        StringBuilder.write(this.builder, '  0  0  0  0  0  0  0  0  0\n');
+        StringBuilder.writeSafe(this.builder, '  0  0  0  0  0  0  0  0  0\n');
 
         StringBuilder.writeSafe(this.builder, StringBuilder.getString(ctab));
         StringBuilder.writeSafe(this.builder, StringBuilder.getString(bonds));
@@ -89,54 +89,6 @@ export class MolEncoder extends LigandExplorer {
         StringBuilder.writeSafe(this.builder, 'M  END\n');
     }
 
-    private getAtoms(source: any, fields: Field<any, any>[], label_atom_id: Field<any, any>, auxiliaryFields: Field<any, any>[], ctab: StringBuilder): string[] {
-        const atoms = [];
-        let index = 0;
-        let id: (string | number)[] | undefined = void 0;
-
-        // is outer loop even needed?
-        l: for (let _c = 0; _c < source.length; _c++) {
-            const src = source[_c];
-            const data = src.data;
-
-            if (src.rowCount === 0) continue;
-
-            const it = src.keys();
-            while (it.hasNext)  {
-                const key = it.move();
-
-                // ensure only a single residue is written
-                // TODO fairly certain this can be done at query level
-                if (!id) {
-                    id = auxiliaryFields.map(af => af.value(key, data, 0));
-                } else {
-                    if (!deepEqual(id, auxiliaryFields.map(af => af.value(key, data, index)))) {
-                        break l;
-                    }
-                }
-
-                const lai = label_atom_id.value(key, data, index) as string;
-                const label = this.getLabel(lai);
-                if (this.skipHydrogen(label)) {
-                    index++;
-                    continue;
-                }
-                atoms.push(lai);
-
-                for (let _f = 0, _fl = fields.length; _f < _fl; _f++) {
-                    const f: Field<any, any> = fields[_f]!;
-                    const v = f.value(key, data, index);
-                    this.writeValue(ctab, v, f.type);
-                }
-
-                StringBuilder.writeSafe(ctab, '  0  0  0  0  0  0  0  0  0  0  0  0\n');
-                index++;
-            }
-        }
-
-        return atoms;
-    }
-
     private writeFullCategory<Ctx>(sb: StringBuilder, category: Category<Ctx>, context?: Ctx) {
         const { instance, source } = getCategoryInstanceData(category, context);
         const fields = instance.fields;
@@ -155,19 +107,6 @@ export class MolEncoder extends LigandExplorer {
         }
     }
 
-    private writeValue(sb: StringBuilder, val: string | number, t: Field.Type, floatPrecision: number = 4) {
-        if (t === Field.Type.Str) {
-            // type_symbol is the only string field - width 2, right-padded
-            StringBuilder.whitespace1(sb);
-            StringBuilder.writePadRight(sb, val as string, 2);
-        } else if (t === Field.Type.Int) {
-            StringBuilder.writeInteger(sb, val as number);
-        } else {
-            // coordinates have width 10 and are left-padded
-            StringBuilder.writePadLeft(sb, (val as number).toFixed(floatPrecision), 10);
-        }
-    }
-
     encode() {
         // write meta-information, do so after ctab
         if (this.error || this.metaInformation) {
@@ -185,7 +124,7 @@ export class MolEncoder extends LigandExplorer {
     constructor(readonly encoder: string, readonly metaInformation: boolean, readonly hydrogens: boolean, readonly terminator: string = '') {
         super(encoder, hydrogens);
 
-        if (metaInformation && !!terminator) {
+        if (metaInformation && !terminator) {
             throw new Error('meta-information cannot be written for MOL files');
         }
         this.meta = StringBuilder.create();

+ 1 - 1
src/servers/model/ligand-writer/mol2.ts

@@ -17,7 +17,7 @@ export namespace Mol2Writer {
     }
 
     export function createEncoder(params?: EncoderParams): Encoder {
-        const { encoderName = 'mol*', metaInformation = false, hydrogens = false } = params || {};
+        const { encoderName = 'mol*', metaInformation = true, hydrogens = true } = params || {};
         return new Mol2Encoder(encoderName, metaInformation, hydrogens);
     }
 }

+ 81 - 0
src/servers/model/ligand-writer/mol2/encoder.ts

@@ -7,10 +7,15 @@
 import { Category } from "../../../../mol-io/writer/cif/encoder";
 import { LigandExplorer } from "../ligand-encoder";
 import { StringBuilder } from "../../../../mol-util";
+import { getCategoryInstanceData } from "../../../../mol-io/writer/cif/encoder/util";
+import { BondType } from "../../../../mol-model/structure/model/types";
 
 // specification: http://chemyang.ccnu.edu.cn/ccb/server/AIMMS/mol2.pdf
+// TODO amide (and real sp/sp2/sp3) support for bonds and SYBYL atom types: see https://www.sdsc.edu/CCMS/Packages/cambridge/pluto/atom_types.html
+// TODO support charges
 export class Mol2Encoder extends LigandExplorer {
     private meta: StringBuilder;
+    private out: StringBuilder;
     private encoded = false;
     private error = false;
 
@@ -19,6 +24,80 @@ export class Mol2Encoder extends LigandExplorer {
             throw new Error('The writer contents have already been encoded, no more writing.');
         }
 
+        if (this.metaInformation && (category.name === 'model_server_result' || category.name === 'model_server_params' || category.name === 'model_server_stats')) {
+            this.writeFullCategory(this.meta, category, context);
+            return;
+        }
+
+        // if error: force writing of meta information
+        if (category.name === 'model_server_error') {
+            this.writeFullCategory(this.meta, category, context);
+            this.error = true;
+            return;
+        }
+
+        // only care about atom_site category when writing SDF
+        if (category.name !== 'atom_site') {
+            return;
+        }
+
+        const a = StringBuilder.create();
+        const b = StringBuilder.create();
+        const { instance, source } = getCategoryInstanceData(category, context);
+
+        // write header
+        const name = this.getName(instance, source);
+        StringBuilder.writeSafe(this.builder, `# Name: ${name}\n# Created by ${this.encoder}\n\n`);
+
+        const bondMap = this.componentData.entries.get(name)!;
+        let bondCount = 0;
+
+        const atoms = this.getAtoms(instance, source);
+        StringBuilder.writeSafe(a, '@<TRIPOS>ATOM\n');
+        StringBuilder.writeSafe(b, '@<TRIPOS>BOND\n');
+        for (let i1 = 0, il = atoms.length; i1 < il; i1++) {
+            const atom = atoms[i1];
+
+            let aromatic = false;
+            bondMap.map.get(atom.id)!.forEach((v, k) => {
+                const i2 = atoms.findIndex(e => e.id === k);
+                const label2 = this.getLabel(k);
+                if (i1 < i2 && atoms.findIndex(e => e.id === k) > -1 && !this.skipHydrogen(label2)) {
+                    const { order, flags } = v;
+                    const ar = flags === BondType.Flag.Aromatic;
+                    if (ar) aromatic = true;
+                    StringBuilder.writeSafe(b, `${++bondCount} ${i1 + 1} ${i2 + 1} ${ar ? 'ar' : order}`);
+                    StringBuilder.newline(b);
+                }
+            });
+
+            const sub = aromatic ? '.ar' : '';
+            StringBuilder.writeSafe(a, `${i1 + 1} ${atom.type_symbol} ${atom.x.toFixed(3)} ${atom.y.toFixed(3)} ${atom.z.toFixed(3)} ${atom.type_symbol}${sub} 1 ${name} 0.000\n`);
+        }
+
+        StringBuilder.writeSafe(this.out, `@<TRIPOS>MOLECULE\n${name}\n${atoms.length} ${bondCount} 0 0 0\nSMALL\nNO_CHARGES\n\n`);
+        StringBuilder.writeSafe(this.out, StringBuilder.getString(a));
+        StringBuilder.writeSafe(this.out, StringBuilder.getString(b));
+        StringBuilder.writeSafe(this.out, `@<TRIPOS>SUBSTRUCTURE\n${name} ${name} 1\n`);
+    }
+
+    private writeFullCategory<Ctx>(sb: StringBuilder, category: Category<Ctx>, context?: Ctx) {
+        const { instance, source } = getCategoryInstanceData(category, context);
+        const fields = instance.fields;
+        const src = source[0];
+        const data = src.data;
+
+        const it = src.keys();
+        const key = it.move();
+        for (let _f = 0; _f < fields.length; _f++) {
+            const f = fields[_f]!;
+
+            StringBuilder.writeSafe(sb, `# ${category.name}.${f.name}: `);
+            const val = f.value(key, data, 0);
+            StringBuilder.writeSafe(sb, val as string);
+            StringBuilder.newline(sb);
+        }
+        StringBuilder.newline(sb);
     }
 
     encode(): void {
@@ -26,6 +105,7 @@ export class Mol2Encoder extends LigandExplorer {
         if (this.error || this.metaInformation) {
             StringBuilder.writeSafe(this.builder, StringBuilder.getString(this.meta));
         }
+        StringBuilder.writeSafe(this.builder, StringBuilder.getString(this.out));
 
         this.encoded = true;
     }
@@ -33,5 +113,6 @@ export class Mol2Encoder extends LigandExplorer {
     constructor(readonly encoder: string, readonly metaInformation: boolean, readonly hydrogens: boolean) {
         super(encoder, hydrogens);
         this.meta = StringBuilder.create();
+        this.out = StringBuilder.create();
     }
 }

+ 1 - 1
src/servers/model/ligand-writer/sdf.ts

@@ -17,7 +17,7 @@ export namespace SdfWriter {
     }
 
     export function createEncoder(params?: EncoderParams): Encoder {
-        const { encoderName = 'mol*', metaInformation = false, hydrogens = false } = params || {};
+        const { encoderName = 'mol*', metaInformation = true, hydrogens = true } = params || {};
         return new MolEncoder(encoderName, metaInformation, hydrogens, '$$$$');
     }
 }