encoder.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. /**
  2. * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
  5. */
  6. import { StringBuilder } from '../../../mol-util';
  7. import Writer from '../writer';
  8. import { Encoder, Category, Field } from '../cif/encoder';
  9. import { getCategoryInstanceData } from '../cif/encoder/util';
  10. import { ComponentBond } from '../../../mol-model-formats/structure/property/bonds/comp';
  11. // specification: http://c4.cabrillo.edu/404/ctfile.pdf
  12. export class SdfEncoder implements Encoder<string> {
  13. private builder: StringBuilder;
  14. private meta: StringBuilder;
  15. private encoded = false;
  16. private error = false;
  17. private componentData: ComponentBond;
  18. readonly isBinary = false;
  19. binaryEncodingProvider = void 0;
  20. setComponentBondData(componentData: ComponentBond) {
  21. this.componentData = componentData;
  22. }
  23. writeTo(stream: Writer) {
  24. const chunks = StringBuilder.getChunks(this.builder);
  25. for (let i = 0, _i = chunks.length; i < _i; i++) {
  26. stream.writeString(chunks[i]);
  27. }
  28. }
  29. getSize() {
  30. return StringBuilder.getSize(this.builder);
  31. }
  32. getData() {
  33. return StringBuilder.getString(this.builder);
  34. }
  35. startDataBlock() {
  36. }
  37. writeCategory<Ctx>(category: Category<Ctx>, context?: Ctx) {
  38. if (this.encoded) {
  39. throw new Error('The writer contents have already been encoded, no more writing.');
  40. }
  41. if (this.metaInformation && (category.name === 'model_server_result' || category.name === 'model_server_params' || category.name === 'model_server_stats')) {
  42. this.writeFullCategory(this.meta, category, context);
  43. return;
  44. }
  45. // if error: force writing of meta information
  46. if (category.name === 'model_server_error') {
  47. this.writeFullCategory(this.meta, category, context);
  48. this.error = true;
  49. return;
  50. }
  51. // only care about atom_site category when writing SDF
  52. if (category.name !== 'atom_site') {
  53. return;
  54. }
  55. // use separate builder because we still need to write Counts and Bonds line
  56. const ctab = StringBuilder.create();
  57. const bonds = StringBuilder.create();
  58. const charges = StringBuilder.create();
  59. // write Atom block and gather data for Bonds and Charges
  60. // 'Specifies the atomic symbol and any mass difference, charge, stereochemistry, and associated hydrogens for each atom.'
  61. const { instance, source } = getCategoryInstanceData(category, context);
  62. const sortedFields = this.getSortedFields(instance);
  63. const label_atom_id = this.getField(instance, 'label_atom_id');
  64. const label_comp_id = this.getField(instance, 'label_comp_id');
  65. const pdbx_PDB_model_num = this.getField(instance, 'pdbx_PDB_model_num');
  66. // write header
  67. const name = label_comp_id.value(source[0].keys().move(), source[0].data, 0) as string;
  68. StringBuilder.write(this.builder, `${name}\nCreated by ${this.encoder}\n\n`);
  69. const bondMap = this.componentData.entries.get(name)!;
  70. let bondCount = 0;
  71. // traverse once to determine all actually present atoms
  72. const atoms = this.getAtoms(source, sortedFields, label_atom_id, pdbx_PDB_model_num, ctab);
  73. atoms.forEach((av, ak) => {
  74. const { id } = this.split(ak);
  75. bondMap.map.get(ak)!.forEach((bv, bk) => {
  76. const { id: partnerId, label: partnerLabel } = this.split(bk);
  77. if (id < partnerId && atoms.has(bk) && !this.skipHydrogen(partnerLabel)) {
  78. const { order } = bv;
  79. StringBuilder.writeIntegerPadLeft(bonds, id, 3);
  80. StringBuilder.writeIntegerPadLeft(bonds, partnerId, 3);
  81. StringBuilder.writeIntegerPadLeft(bonds, order, 3);
  82. StringBuilder.writeSafe(bonds, ' 0 0 0 0\n');
  83. // TODO 2nd value: Single bonds: 0 = not stereo, 1 = Up, 4 = Either, 6 = Down,
  84. // Double bonds: 0 = Use x-, y-, z-coords from atom block to determine cis or trans, 3 = Cis or trans (either) double bond
  85. bondCount++;
  86. }
  87. });
  88. });
  89. // write counts line
  90. // 'Important specifications here relate to the number of atoms, bonds, and atom lists, the chiral flag setting, and the Ctab version.'
  91. StringBuilder.writeIntegerPadLeft(this.builder, atoms.size, 3);
  92. StringBuilder.writeIntegerPadLeft(this.builder, bondCount, 3);
  93. StringBuilder.write(this.builder, ' 0 0 0 0 0 0 0999 V2000\n'); // TODO 2nd value: chiral flag: 0=not chiral, 1=chiral
  94. StringBuilder.writeSafe(this.builder, StringBuilder.getString(ctab));
  95. StringBuilder.writeSafe(this.builder, StringBuilder.getString(bonds));
  96. StringBuilder.writeSafe(this.builder, StringBuilder.getString(charges)); // TODO charges
  97. StringBuilder.writeSafe(this.builder, 'M END\n');
  98. }
  99. private getAtoms(source: any, fields: Field<any, any>[], label_atom_id: Field<any, any>, pdbx_PDB_model_num: Field<any, any>, ctab: StringBuilder): Map<string, { id: number, label: string }> {
  100. const atoms = new Map<string, any>();
  101. let index = 0;
  102. for (let _c = 0; _c < source.length; _c++) {
  103. const src = source[_c];
  104. const data = src.data;
  105. if (src.rowCount === 0) continue;
  106. const it = src.keys();
  107. while (it.hasNext) {
  108. const key = it.move();
  109. if (pdbx_PDB_model_num.value(key, data, index) !== 1) {
  110. continue; // TODO model support
  111. }
  112. const laiv = label_atom_id.value(key, data, index) as string;
  113. const lai = this.split(laiv);
  114. if (this.skipHydrogen(lai.label)) {
  115. index++;
  116. continue;
  117. }
  118. atoms.set(laiv, lai);
  119. for (let _f = 0, _fl = fields.length; _f < _fl; _f++) {
  120. const f: Field<any, any> = fields[_f]!;
  121. const v = f.value(key, data, index);
  122. this.writeValue(ctab, v, f.type);
  123. }
  124. StringBuilder.writeSafe(ctab, ' 0 0 0 0 0 0 0 0 0 0 0 0\n');
  125. index++;
  126. }
  127. }
  128. return atoms;
  129. }
  130. private skipHydrogen(label: string) {
  131. if (this.hydrogens) {
  132. return false;
  133. }
  134. return label.startsWith('H');
  135. }
  136. private split(s: string) {
  137. return {
  138. id: Number.parseInt(s.replace(/[^0-9]+/, '')),
  139. label: s.replace(/[^A-Z]+/, '')
  140. }
  141. }
  142. private writeFullCategory<Ctx>(sb: StringBuilder, category: Category<Ctx>, context?: Ctx) {
  143. const { instance, source } = getCategoryInstanceData(category, context);
  144. const fields = instance.fields;
  145. const src = source[0];
  146. const data = src.data;
  147. const it = src.keys();
  148. const key = it.move();
  149. for (let _f = 0; _f < fields.length; _f++) {
  150. const f = fields[_f]!;
  151. StringBuilder.writeSafe(sb, `> <${category.name}.${f.name}>\n`);
  152. const val = f.value(key, data, 0);
  153. StringBuilder.writeSafe(sb, val as string);
  154. StringBuilder.writeSafe(sb, '\n\n');
  155. }
  156. }
  157. private writeValue(sb: StringBuilder, val: string | number, t: Field.Type, floatPrecision: number = 4) {
  158. if (t === Field.Type.Str) {
  159. // type_symbol is the only string field - width 2, right-padded
  160. StringBuilder.whitespace1(sb);
  161. StringBuilder.writePadRight(sb, val as string, 2);
  162. } else if (t === Field.Type.Int) {
  163. StringBuilder.writeInteger(sb, val as number);
  164. } else {
  165. // coordinates have width 10 and are left-padded
  166. StringBuilder.writePadLeft(sb, (val as number).toFixed(floatPrecision), 10);
  167. }
  168. }
  169. private getSortedFields<Ctx>(instance: Category.Instance<Ctx>) {
  170. return ['Cartn_x', 'Cartn_y', 'Cartn_z', 'type_symbol']
  171. .map(n => this.getField(instance, n));
  172. }
  173. private getField<Ctx>(instance: Category.Instance<Ctx>, name: string) {
  174. return instance.fields.find(f => f.name === name)!;
  175. }
  176. encode() {
  177. // write meta-information, do so after ctab
  178. if (this.error || this.metaInformation) {
  179. StringBuilder.writeSafe(this.builder, StringBuilder.getString(this.meta));
  180. }
  181. // terminate file
  182. StringBuilder.writeSafe(this.builder, '$$$$\n');
  183. this.encoded = true;
  184. }
  185. setFilter(filter?: Category.Filter) {}
  186. setFormatter(formatter?: Category.Formatter) {}
  187. isCategoryIncluded(name: string) {
  188. return true;
  189. }
  190. constructor(readonly encoder: string, readonly metaInformation: boolean, readonly hydrogens: boolean) {
  191. this.builder = StringBuilder.create();
  192. this.meta = StringBuilder.create();
  193. }
  194. }