|
@@ -9,10 +9,14 @@ import { LigandEncoder } from '../ligand-encoder';
|
|
|
import { StringBuilder } from '../../../mol-util';
|
|
|
import { getCategoryInstanceData } from '../cif/encoder/util';
|
|
|
import { BondType } from '../../../mol-model/structure/model/types';
|
|
|
+import { ComponentBond } from '../../../mol-model-formats/structure/property/bonds/chem_comp';
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+const NON_METAL_ATOMS = 'H D B C N O F Si P S Cl As Se Br Te I At He Ne Ar Kr Xe Rn'.split(' ');
|
|
|
+type BondMap = Map<string, { order: number, flags: number }>;
|
|
|
|
|
|
|
|
|
-
|
|
|
-
|
|
|
export class Mol2Encoder extends LigandEncoder {
|
|
|
private out: StringBuilder;
|
|
|
|
|
@@ -25,36 +29,258 @@ export class Mol2Encoder extends LigandEncoder {
|
|
|
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)!;
|
|
|
+ const bondMap = this.componentBondData.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.label_atom_id)!.forEach((v, k) => {
|
|
|
- const i2 = atoms.findIndex(e => e.label_atom_id === k);
|
|
|
- const label2 = this.getLabel(k);
|
|
|
- if (i1 < i2 && atoms.findIndex(e => e.label_atom_id === k) > -1 && !this.skipHydrogen(label2)) {
|
|
|
- const { order, flags } = v;
|
|
|
- const ar = flags === BondType.Flag.Aromatic;
|
|
|
- if (ar) aromatic = true;
|
|
|
+ atoms.forEach((atom1, label_atom_id1) => {
|
|
|
+ const { index: i1 } = atom1;
|
|
|
+ bondMap.map.get(label_atom_id1)!.forEach((bond, label_atom_id2) => {
|
|
|
+ const atom2 = atoms.get(label_atom_id2);
|
|
|
+ if (!atom2) return;
|
|
|
+
|
|
|
+ const { index: i2, type_symbol: type_symbol2 } = atom2;
|
|
|
+ if (i1 < i2 && !this.skipHydrogen(type_symbol2)) {
|
|
|
+ const { order, flags } = bond;
|
|
|
+ const ar = BondType.is(BondType.Flag.Aromatic, flags);
|
|
|
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.Cartn_x.toFixed(3)} ${atom.Cartn_y.toFixed(3)} ${atom.Cartn_z.toFixed(3)} ${atom.type_symbol}${sub} 1 ${name} 0.000\n`);
|
|
|
- }
|
|
|
+ const sybyl = this.mapToSybyl(label_atom_id1, atom1.type_symbol, bondMap);
|
|
|
+ StringBuilder.writeSafe(a, `${i1 + 1} ${label_atom_id1} ${atom1.Cartn_x.toFixed(3)} ${atom1.Cartn_y.toFixed(3)} ${atom1.Cartn_z.toFixed(3)} ${sybyl} 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, `@<TRIPOS>MOLECULE\n${name}\n${atoms.size} ${bondCount} 1\n****\n****\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`);
|
|
|
+ StringBuilder.writeSafe(this.out, `@<TRIPOS>SUBSTRUCTURE\n1 ${name} 1\n`);
|
|
|
+ }
|
|
|
+
|
|
|
+ private count<K, V, C>(map: Map<K, V>, ctx: C, predicate: (k: K, v: V, ctx: C) => boolean): number {
|
|
|
+ let count = 0;
|
|
|
+ const iter = map.entries();
|
|
|
+ let result = iter.next();
|
|
|
+ while (!result.done) {
|
|
|
+ if (predicate(result.value[0], result.value[1], ctx)) {
|
|
|
+ count++;
|
|
|
+ }
|
|
|
+ result = iter.next();
|
|
|
+ }
|
|
|
+ return count;
|
|
|
+ }
|
|
|
+
|
|
|
+ private orderSum(map: BondMap): number {
|
|
|
+ let sum = 0;
|
|
|
+ const iter = map.values();
|
|
|
+ let result = iter.next();
|
|
|
+ while (!result.done) {
|
|
|
+ sum += result.value.order;
|
|
|
+ result = iter.next();
|
|
|
+ }
|
|
|
+ return sum;
|
|
|
+ }
|
|
|
+
|
|
|
+ private isNonMetalBond(label_atom_id: string): boolean {
|
|
|
+ for (const a of NON_METAL_ATOMS) {
|
|
|
+ if (label_atom_id.startsWith(a)) return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private extractNonmets(map: BondMap): BondMap {
|
|
|
+ const ret = new Map<string, { order: number, flags: number }>();
|
|
|
+ const iter = map.entries();
|
|
|
+ let result = iter.next();
|
|
|
+ while (!result.done) {
|
|
|
+ const [k, v] = result.value;
|
|
|
+ if (this.isNonMetalBond(k)) {
|
|
|
+ ret.set(k, v);
|
|
|
+ }
|
|
|
+ result = iter.next();
|
|
|
+ }
|
|
|
+ return ret;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ private mapToSybyl(label_atom_id1: string, type_symbol1: string, bondMap: ComponentBond.Entry) {
|
|
|
+
|
|
|
+
|
|
|
+ if (type_symbol1 === 'D') return 'H';
|
|
|
+ if (type_symbol1 === 'P') return 'P.3';
|
|
|
+ if (type_symbol1 === 'Co' || type_symbol1 === 'Ru') return type_symbol1 + '.oh';
|
|
|
+
|
|
|
+ const bonds = bondMap.map.get(label_atom_id1)!;
|
|
|
+ const numBonds = bonds.size;
|
|
|
+
|
|
|
+ if (type_symbol1 === 'Ti' || type_symbol1 === 'Cr') {
|
|
|
+ return type_symbol1 + (numBonds <= 4 ? '.th' : '.oh');
|
|
|
+ }
|
|
|
+ if (type_symbol1 === 'C') {
|
|
|
+ if (numBonds >= 4 && this.count(bonds, this, (_k, v) => v.order === 1) >= 4) return 'C.3';
|
|
|
+ if (numBonds === 3 && this.isCat(bonds, bondMap)) return 'C.cat';
|
|
|
+ if (numBonds >= 2 && this.count(bonds, this, (_k, v) => BondType.is(BondType.Flag.Aromatic, v.flags)) >= 2) return 'C.ar';
|
|
|
+ if ((numBonds === 1 || numBonds === 2) && this.count(bonds, this, (_k, v) => v.order === 3)) return 'C.1'; // 1.6.4, 3i04/ligand?encoding=mol2&auth_asym_id=C&auth_seq_id=900 (CYN)
|
|
|
+ return 'C.2';
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ const nonmets = this.count(bonds, this, (k, _v, ctx) => ctx.isNonMetalBond(k)) === bonds.size ? bonds : this.extractNonmets(bonds);
|
|
|
+ const numNonmets = nonmets.size;
|
|
|
+
|
|
|
+ if (type_symbol1 === 'O') {
|
|
|
+ if (numNonmets === 1) {
|
|
|
+ if (this.isOC(nonmets, bondMap)) return 'O.co2';
|
|
|
+ if (this.isOP(nonmets, bondMap)) return 'O.co2';
|
|
|
+ }
|
|
|
+ if (numNonmets >= 2 && this.count(bonds, this, (_k, v) => v.order === 1) === bonds.size) return 'O.3';
|
|
|
+ return 'O.2';
|
|
|
+ }
|
|
|
+ if (type_symbol1 === 'N') {
|
|
|
+ if (numNonmets === 4 && this.count(nonmets, this, (_k, v) => v.order === 1) === 4) return 'N.4';
|
|
|
+ if (numBonds >= 2 && this.count(bonds, this, (_k, v) => BondType.is(BondType.Flag.Aromatic, v.flags)) >= 2) return 'N.ar';
|
|
|
+ if (numNonmets === 1 && this.count(nonmets, this, (_k, v) => v.order === 3)) return 'N.1';
|
|
|
+ if (numNonmets === 2 && this.orderSum(nonmets) === 4) return 'N.1';
|
|
|
+ if (numNonmets === 3 && this.hasCOCS(nonmets, bondMap)) return 'N.am';
|
|
|
+ if (numNonmets === 3) {
|
|
|
+ if (this.count(nonmets, this, (_k, v) => v.order > 1) === 1) return 'N.pl3';
|
|
|
+ if (this.count(nonmets, this, (_k, v) => v.order === 1) === 3) {
|
|
|
+ if (this.isNpl3(nonmets, bondMap)) return 'N.pl3';
|
|
|
+ }
|
|
|
+ return 'N.3';
|
|
|
+ }
|
|
|
+ return 'N.2';
|
|
|
+ }
|
|
|
+ if (type_symbol1 === 'S') {
|
|
|
+ if (numNonmets === 3 && this.countOfOxygenWithSingleNonmet(nonmets, bondMap) === 1) return 'S.o';
|
|
|
+ if (numNonmets === 4 && this.countOfOxygenWithSingleNonmet(nonmets, bondMap) === 2) return 'S.o2';
|
|
|
+ if (numNonmets >= 2 && this.count(bonds, this, (_k, v) => v.order === 1) >= 2) return 'S.3';
|
|
|
+ return 'S.2';
|
|
|
+ }
|
|
|
+ return type_symbol1;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ private isNpl3(nonmets: BondMap, bondMap: ComponentBond.Entry): boolean {
|
|
|
+ const iter = nonmets.keys();
|
|
|
+ let result = iter.next();
|
|
|
+ while (!result.done) {
|
|
|
+ const label_atom_id = result.value;
|
|
|
+ const adjacentBonds = bondMap.map.get(label_atom_id)!;
|
|
|
+ if (this.count(adjacentBonds, this, (_k, v) => v.order > 1 || BondType.is(BondType.Flag.Aromatic, v.flags))) {
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ result = iter.next();
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ private isOC(nonmets: BondMap, bondMap: ComponentBond.Entry): boolean {
|
|
|
+ const nonmet = nonmets.entries().next()!.value as [string, { order: number, flags: number }];
|
|
|
+ if (!nonmet[0].startsWith('C')) return false;
|
|
|
+ const carbonBonds = bondMap.map.get(nonmet[0])!;
|
|
|
+ if (carbonBonds.size !== 3) return false;
|
|
|
+
|
|
|
+ let count = 0;
|
|
|
+ const iter = carbonBonds.keys();
|
|
|
+ let result = iter.next();
|
|
|
+ while (!result.done) {
|
|
|
+ const label_atom_id = result.value;
|
|
|
+ if (label_atom_id.startsWith('O')) {
|
|
|
+ const adjacentBonds = bondMap.map.get(label_atom_id)!;
|
|
|
+ if (this.count(adjacentBonds, this, (k, _v, ctx) => ctx.isNonMetalBond(k)) === 1) count++;
|
|
|
+ }
|
|
|
+ result = iter.next();
|
|
|
+ }
|
|
|
+ return count === 2;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ private isOP(nonmets: BondMap, bondMap: ComponentBond.Entry): boolean {
|
|
|
+ const nonmet = nonmets.entries().next()!.value as [string, { order: number, flags: number }];
|
|
|
+ if (!nonmet[0].startsWith('P')) return false;
|
|
|
+ const phosphorusBonds = bondMap.map.get(nonmet[0])!;
|
|
|
+ if (phosphorusBonds.size < 2) return false;
|
|
|
+
|
|
|
+ let count = 0;
|
|
|
+ const iter = phosphorusBonds.keys();
|
|
|
+ let result = iter.next();
|
|
|
+ while (!result.done) {
|
|
|
+ const label_atom_id = result.value;
|
|
|
+ if (label_atom_id.startsWith('O')) {
|
|
|
+ const adjacentBonds = bondMap.map.get(label_atom_id)!;
|
|
|
+ if (this.count(adjacentBonds, this, (k, _v, ctx) => ctx.isNonMetalBond(k)) === 1) count++;
|
|
|
+ }
|
|
|
+ result = iter.next();
|
|
|
+ }
|
|
|
+ return count >= 2;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ private isCat(currentBondMap: BondMap, bondMap: ComponentBond.Entry): boolean {
|
|
|
+ const iter1 = currentBondMap.keys();
|
|
|
+ let result1 = iter1.next();
|
|
|
+ while (!result1.done) {
|
|
|
+ const label_atom_id = result1.value;
|
|
|
+ if (!label_atom_id.startsWith('N')) return false;
|
|
|
+
|
|
|
+ const adjacentBonds = bondMap.map.get(label_atom_id)!;
|
|
|
+ if (adjacentBonds.size < 2) return false;
|
|
|
+
|
|
|
+ const iter2 = adjacentBonds.keys();
|
|
|
+ let result2 = iter2.next();
|
|
|
+ while (!result2.done) {
|
|
|
+ if (result2.value.startsWith('O')) return false;
|
|
|
+ result2 = iter2.next();
|
|
|
+ }
|
|
|
+ result1 = iter1.next();
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ private countOfOxygenWithSingleNonmet(nonmets: BondMap, bondMap: ComponentBond.Entry): number {
|
|
|
+ let count = 0;
|
|
|
+ const iter = nonmets.keys();
|
|
|
+ let result = iter.next();
|
|
|
+ while (!result.done) {
|
|
|
+ const label_atom_id = result.value;
|
|
|
+ if (label_atom_id.startsWith('O')) {
|
|
|
+ const adjacentBonds = bondMap.map.get(label_atom_id)!;
|
|
|
+ if (this.count(adjacentBonds, this, (k, _v, ctx) => ctx.isNonMetalBond(k))) count++;
|
|
|
+ }
|
|
|
+ result = iter.next();
|
|
|
+ }
|
|
|
+ return count;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ private hasCOCS(nonmets: BondMap, bondMap: ComponentBond.Entry): boolean {
|
|
|
+ const iter = nonmets.keys();
|
|
|
+ let result = iter.next();
|
|
|
+ while (!result.done) {
|
|
|
+ const label_atom_id = result.value;
|
|
|
+ if (label_atom_id.startsWith('C')) {
|
|
|
+ const adjacentBonds = bondMap.map.get(label_atom_id)!;
|
|
|
+ if (this.count(adjacentBonds, this, (k, v) => k.startsWith('O') || k.startsWith('S') && v.order === 2)) return true;
|
|
|
+ }
|
|
|
+ result = iter.next();
|
|
|
+ }
|
|
|
+ return false;
|
|
|
}
|
|
|
|
|
|
protected writeFullCategory<Ctx>(sb: StringBuilder, category: Category<Ctx>, context?: Ctx) {
|