model.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. /**
  2. * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  6. */
  7. import { parsePDB } from 'mol-io/reader/pdb/parser';
  8. import { Vec3 } from 'mol-math/linear-algebra';
  9. import { trajectoryFromMmCIF } from 'mol-model-formats/structure/mmcif';
  10. import { trajectoryFromPDB } from 'mol-model-formats/structure/pdb';
  11. import { Model, ModelSymmetry, Queries, QueryContext, Structure, StructureQuery, StructureSelection as Sel, StructureSymmetry, QueryFn } from 'mol-model/structure';
  12. import { Assembly } from 'mol-model/structure/model/properties/symmetry';
  13. import { PluginContext } from 'mol-plugin/context';
  14. import { MolScriptBuilder } from 'mol-script/language/builder';
  15. import Expression from 'mol-script/language/expression';
  16. import { compile } from 'mol-script/runtime/query/compiler';
  17. import { StateObject, StateTransformer } from 'mol-state';
  18. import { RuntimeContext, Task } from 'mol-task';
  19. import { ParamDefinition as PD } from 'mol-util/param-definition';
  20. import { stringToWords } from 'mol-util/string';
  21. import { PluginStateObject as SO, PluginStateTransform } from '../objects';
  22. import { trajectoryFromGRO } from 'mol-model-formats/structure/gro';
  23. import { parseGRO } from 'mol-io/reader/gro/parser';
  24. import { parseMolScript } from 'mol-script/language/parser';
  25. import { transpileMolScript } from 'mol-script/script/mol-script/symbols';
  26. import { shapeFromPly } from 'mol-model-formats/shape/ply';
  27. export { TrajectoryFromBlob };
  28. export { TrajectoryFromMmCif };
  29. export { TrajectoryFromPDB };
  30. export { TrajectoryFromGRO };
  31. export { ModelFromTrajectory };
  32. export { StructureFromModel };
  33. export { StructureAssemblyFromModel };
  34. export { StructureSymmetryFromModel };
  35. export { StructureSelection };
  36. export { UserStructureSelection };
  37. export { StructureComplexElement };
  38. export { CustomModelProperties };
  39. type TrajectoryFromBlob = typeof TrajectoryFromBlob
  40. const TrajectoryFromBlob = PluginStateTransform.BuiltIn({
  41. name: 'trajectory-from-blob',
  42. display: { name: 'Parse Blob', description: 'Parse format blob into a single trajectory.' },
  43. from: SO.Format.Blob,
  44. to: SO.Molecule.Trajectory
  45. })({
  46. apply({ a }) {
  47. return Task.create('Parse Format Blob', async ctx => {
  48. const models: Model[] = [];
  49. for (const e of a.data) {
  50. if (e.kind !== 'cif') continue;
  51. const block = e.data.blocks[0];
  52. const xs = await trajectoryFromMmCIF(block).runInContext(ctx);
  53. if (xs.length === 0) throw new Error('No models found.');
  54. for (const x of xs) models.push(x);
  55. }
  56. const props = { label: `Trajectory`, description: `${models.length} model${models.length === 1 ? '' : 's'}` };
  57. return new SO.Molecule.Trajectory(models, props);
  58. });
  59. }
  60. });
  61. type TrajectoryFromMmCif = typeof TrajectoryFromMmCif
  62. const TrajectoryFromMmCif = PluginStateTransform.BuiltIn({
  63. name: 'trajectory-from-mmcif',
  64. display: { name: 'Trajectory from mmCIF', description: 'Identify and create all separate models in the specified CIF data block' },
  65. from: SO.Format.Cif,
  66. to: SO.Molecule.Trajectory,
  67. params(a) {
  68. if (!a) {
  69. return {
  70. blockHeader: PD.Optional(PD.Text(void 0, { description: 'Header of the block to parse. If none is specifed, the 1st data block in the file is used.' }))
  71. };
  72. }
  73. const { blocks } = a.data;
  74. return {
  75. blockHeader: PD.Optional(PD.Select(blocks[0] && blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]), { description: 'Header of the block to parse' }))
  76. };
  77. }
  78. })({
  79. isApplicable: a => a.data.blocks.length > 0,
  80. apply({ a, params }) {
  81. return Task.create('Parse mmCIF', async ctx => {
  82. const header = params.blockHeader || a.data.blocks[0].header;
  83. const block = a.data.blocks.find(b => b.header === header);
  84. if (!block) throw new Error(`Data block '${[header]}' not found.`);
  85. const models = await trajectoryFromMmCIF(block).runInContext(ctx);
  86. if (models.length === 0) throw new Error('No models found.');
  87. const props = { label: models[0].label, description: `${models.length} model${models.length === 1 ? '' : 's'}` };
  88. return new SO.Molecule.Trajectory(models, props);
  89. });
  90. }
  91. });
  92. type TrajectoryFromPDB = typeof TrajectoryFromPDB
  93. const TrajectoryFromPDB = PluginStateTransform.BuiltIn({
  94. name: 'trajectory-from-pdb',
  95. display: { name: 'Parse PDB', description: 'Parse PDB string and create trajectory.' },
  96. from: [SO.Data.String],
  97. to: SO.Molecule.Trajectory
  98. })({
  99. apply({ a }) {
  100. return Task.create('Parse PDB', async ctx => {
  101. const parsed = await parsePDB(a.data).runInContext(ctx);
  102. if (parsed.isError) throw new Error(parsed.message);
  103. const models = await trajectoryFromPDB(parsed.result).runInContext(ctx);
  104. const props = { label: models[0].label, description: `${models.length} model${models.length === 1 ? '' : 's'}` };
  105. return new SO.Molecule.Trajectory(models, props);
  106. });
  107. }
  108. });
  109. type TrajectoryFromGRO = typeof TrajectoryFromGRO
  110. const TrajectoryFromGRO = PluginStateTransform.BuiltIn({
  111. name: 'trajectory-from-gro',
  112. display: { name: 'Parse GRO', description: 'Parse GRO string and create trajectory.' },
  113. from: [SO.Data.String],
  114. to: SO.Molecule.Trajectory
  115. })({
  116. apply({ a }) {
  117. return Task.create('Parse GRO', async ctx => {
  118. const parsed = await parseGRO(a.data).runInContext(ctx);
  119. if (parsed.isError) throw new Error(parsed.message);
  120. const models = await trajectoryFromGRO(parsed.result).runInContext(ctx);
  121. const props = { label: models[0].label, description: `${models.length} model${models.length === 1 ? '' : 's'}` };
  122. return new SO.Molecule.Trajectory(models, props);
  123. });
  124. }
  125. });
  126. const plus1 = (v: number) => v + 1, minus1 = (v: number) => v - 1;
  127. type ModelFromTrajectory = typeof ModelFromTrajectory
  128. const ModelFromTrajectory = PluginStateTransform.BuiltIn({
  129. name: 'model-from-trajectory',
  130. display: { name: 'Molecular Model', description: 'Create a molecular model from specified index in a trajectory.' },
  131. from: SO.Molecule.Trajectory,
  132. to: SO.Molecule.Model,
  133. params: a => {
  134. if (!a) {
  135. return { modelIndex: PD.Numeric(0, {}, { description: 'Zero-based index of the model' }) };
  136. }
  137. return { modelIndex: PD.Converted(plus1, minus1, PD.Numeric(1, { min: 1, max: a.data.length, step: 1 }, { description: 'Model Index' })) }
  138. }
  139. })({
  140. isApplicable: a => a.data.length > 0,
  141. apply({ a, params }) {
  142. if (params.modelIndex < 0 || params.modelIndex >= a.data.length) throw new Error(`Invalid modelIndex ${params.modelIndex}`);
  143. const model = a.data[params.modelIndex];
  144. const props = a.data.length === 1
  145. ? { label: `${model.label}` }
  146. : { label: `${model.label}:${model.modelNum}`, description: `Model ${params.modelIndex + 1} of ${a.data.length}` };
  147. return new SO.Molecule.Model(model, props);
  148. }
  149. });
  150. type StructureFromModel = typeof StructureFromModel
  151. const StructureFromModel = PluginStateTransform.BuiltIn({
  152. name: 'structure-from-model',
  153. display: { name: 'Structure from Model', description: 'Create a molecular structure from the specified model.' },
  154. from: SO.Molecule.Model,
  155. to: SO.Molecule.Structure
  156. })({
  157. apply({ a }) {
  158. let s = Structure.ofModel(a.data);
  159. const props = { label: a.data.label, description: s.elementCount === 1 ? '1 element' : `${s.elementCount} elements` };
  160. return new SO.Molecule.Structure(s, props);
  161. }
  162. });
  163. function structureDesc(s: Structure) {
  164. return s.elementCount === 1 ? '1 element' : `${s.elementCount} elements`;
  165. }
  166. type StructureAssemblyFromModel = typeof StructureAssemblyFromModel
  167. const StructureAssemblyFromModel = PluginStateTransform.BuiltIn({
  168. name: 'structure-assembly-from-model',
  169. display: { name: 'Structure Assembly', description: 'Create a molecular structure assembly.' },
  170. from: SO.Molecule.Model,
  171. to: SO.Molecule.Structure,
  172. params(a) {
  173. if (!a) {
  174. return { id: PD.Optional(PD.Text('', { label: 'Assembly Id', description: 'Assembly Id. Value \'deposited\' can be used to specify deposited asymmetric unit.' })) };
  175. }
  176. const model = a.data;
  177. const ids = model.symmetry.assemblies.map(a => [a.id, `${a.id}: ${stringToWords(a.details)}`] as [string, string]);
  178. ids.push(['deposited', 'Deposited']);
  179. return { id: PD.Optional(PD.Select(ids[0][0], ids, { label: 'Asm Id', description: 'Assembly Id' })) };
  180. }
  181. })({
  182. apply({ a, params }, plugin: PluginContext) {
  183. return Task.create('Build Assembly', async ctx => {
  184. const model = a.data;
  185. let id = params.id;
  186. let asm: Assembly | undefined = void 0;
  187. // if no id is specified, use the 1st assembly.
  188. if (!id && model.symmetry.assemblies.length !== 0) {
  189. id = model.symmetry.assemblies[0].id;
  190. }
  191. if (model.symmetry.assemblies.length === 0) {
  192. if (id !== 'deposited') {
  193. plugin.log.warn(`Model '${a.data.label}' has no assembly, returning deposited structure.`);
  194. }
  195. } else {
  196. asm = ModelSymmetry.findAssembly(model, id || '');
  197. if (!asm) {
  198. plugin.log.warn(`Model '${a.data.label}' has no assembly called '${id}', returning deposited structure.`);
  199. }
  200. }
  201. const base = Structure.ofModel(model);
  202. if (!asm) {
  203. const label = { label: a.data.label, description: structureDesc(base) };
  204. return new SO.Molecule.Structure(base, label);
  205. }
  206. id = asm.id;
  207. const s = await StructureSymmetry.buildAssembly(base, id!).runInContext(ctx);
  208. const props = { label: `Assembly ${id}`, description: structureDesc(s) };
  209. return new SO.Molecule.Structure(s, props);
  210. })
  211. }
  212. });
  213. type StructureSymmetryFromModel = typeof StructureSymmetryFromModel
  214. const StructureSymmetryFromModel = PluginStateTransform.BuiltIn({
  215. name: 'structure-symmetry-from-model',
  216. display: { name: 'Structure Symmetry', description: 'Create a molecular structure symmetry.' },
  217. from: SO.Molecule.Model,
  218. to: SO.Molecule.Structure,
  219. params(a) {
  220. return {
  221. ijkMin: PD.Vec3(Vec3.create(-1, -1, -1), { label: 'Min IJK', fieldLabels: { x: 'I', y: 'J', z: 'K' } }),
  222. ijkMax: PD.Vec3(Vec3.create(1, 1, 1), { label: 'Max IJK', fieldLabels: { x: 'I', y: 'J', z: 'K' } })
  223. }
  224. }
  225. })({
  226. apply({ a, params }, plugin: PluginContext) {
  227. return Task.create('Build Symmetry', async ctx => {
  228. const { ijkMin, ijkMax } = params
  229. const model = a.data;
  230. const base = Structure.ofModel(model);
  231. const s = await StructureSymmetry.buildSymmetryRange(base, ijkMin, ijkMax).runInContext(ctx);
  232. const props = { label: `Symmetry [${ijkMin}] to [${ijkMax}]`, description: structureDesc(s) };
  233. return new SO.Molecule.Structure(s, props);
  234. })
  235. }
  236. });
  237. type StructureSelection = typeof StructureSelection
  238. const StructureSelection = PluginStateTransform.BuiltIn({
  239. name: 'structure-selection',
  240. display: { name: 'Structure Selection', description: 'Create a molecular structure from the specified query expression.' },
  241. from: SO.Molecule.Structure,
  242. to: SO.Molecule.Structure,
  243. params: {
  244. query: PD.Value<Expression>(MolScriptBuilder.struct.generator.all, { isHidden: true }),
  245. label: PD.Optional(PD.Text('', { isHidden: true }))
  246. }
  247. })({
  248. apply({ a, params, cache }) {
  249. const compiled = compile<Sel>(params.query);
  250. (cache as { compiled: QueryFn<Sel> }).compiled = compiled;
  251. (cache as { source: Structure }).source = a.data;
  252. const result = compiled(new QueryContext(a.data));
  253. const s = Sel.unionStructure(result);
  254. if (s.elementCount === 0) return StateObject.Null;
  255. const props = { label: `${params.label || 'Selection'}`, description: structureDesc(s) };
  256. return new SO.Molecule.Structure(s, props);
  257. },
  258. update: ({ a, b, oldParams, newParams, cache }) => {
  259. if (oldParams.query !== newParams.query) return StateTransformer.UpdateResult.Recreate;
  260. if ((cache as { source: Structure }).source === a.data) {
  261. return StateTransformer.UpdateResult.Unchanged;
  262. }
  263. (cache as { source: Structure }).source = a.data;
  264. if (updateStructureFromQuery((cache as { compiled: QueryFn<Sel> }).compiled, a.data, b, newParams.label)) {
  265. return StateTransformer.UpdateResult.Updated;
  266. }
  267. return StateTransformer.UpdateResult.Null;
  268. }
  269. });
  270. type UserStructureSelection = typeof UserStructureSelection
  271. const UserStructureSelection = PluginStateTransform.BuiltIn({
  272. name: 'user-structure-selection',
  273. display: { name: 'Structure Selection', description: 'Create a molecular structure from the specified query expression.' },
  274. from: SO.Molecule.Structure,
  275. to: SO.Molecule.Structure,
  276. params: {
  277. query: PD.ScriptExpression({ language: 'mol-script', expression: '(sel.atom.atom-groups :residue-test (= atom.resname ALA))' }),
  278. label: PD.Optional(PD.Text(''))
  279. }
  280. })({
  281. apply({ a, params, cache }) {
  282. const parsed = parseMolScript(params.query.expression);
  283. if (parsed.length === 0) throw new Error('No query');
  284. const query = transpileMolScript(parsed[0]);
  285. const compiled = compile<Sel>(query);
  286. (cache as { compiled: QueryFn<Sel> }).compiled = compiled;
  287. (cache as { source: Structure }).source = a.data;
  288. const result = compiled(new QueryContext(a.data));
  289. const s = Sel.unionStructure(result);
  290. const props = { label: `${params.label || 'Selection'}`, description: structureDesc(s) };
  291. return new SO.Molecule.Structure(s, props);
  292. },
  293. update: ({ a, b, oldParams, newParams, cache }) => {
  294. if (oldParams.query.language !== newParams.query.language || oldParams.query.expression !== newParams.query.expression) {
  295. return StateTransformer.UpdateResult.Recreate;
  296. }
  297. if ((cache as { source: Structure }).source === a.data) {
  298. return StateTransformer.UpdateResult.Unchanged;
  299. }
  300. (cache as { source: Structure }).source = a.data;
  301. updateStructureFromQuery((cache as { compiled: QueryFn<Sel> }).compiled, a.data, b, newParams.label);
  302. return StateTransformer.UpdateResult.Updated;
  303. }
  304. });
  305. function updateStructureFromQuery(query: QueryFn<Sel>, src: Structure, obj: SO.Molecule.Structure, label?: string) {
  306. const result = query(new QueryContext(src));
  307. const s = Sel.unionStructure(result);
  308. if (s.elementCount === 0) {
  309. return false;
  310. }
  311. obj.label = `${label || 'Selection'}`;
  312. obj.description = structureDesc(s);
  313. obj.data = s;
  314. return true;
  315. }
  316. namespace StructureComplexElement {
  317. export type Types = 'atomic-sequence' | 'water' | 'atomic-het' | 'spheres'
  318. }
  319. const StructureComplexElementTypes: [StructureComplexElement.Types, StructureComplexElement.Types][] = ['atomic-sequence', 'water', 'atomic-het', 'spheres'].map(t => [t, t] as any);
  320. type StructureComplexElement = typeof StructureComplexElement
  321. const StructureComplexElement = PluginStateTransform.BuiltIn({
  322. name: 'structure-complex-element',
  323. display: { name: 'Complex Element', description: 'Create a molecular structure from the specified model.' },
  324. from: SO.Molecule.Structure,
  325. to: SO.Molecule.Structure,
  326. params: { type: PD.Select<StructureComplexElement.Types>('atomic-sequence', StructureComplexElementTypes, { isHidden: true }) }
  327. })({
  328. apply({ a, params }) {
  329. // TODO: update function.
  330. let query: StructureQuery, label: string;
  331. switch (params.type) {
  332. case 'atomic-sequence': query = Queries.internal.atomicSequence(); label = 'Sequence'; break;
  333. case 'water': query = Queries.internal.water(); label = 'Water'; break;
  334. case 'atomic-het': query = Queries.internal.atomicHet(); label = 'HET Groups/Ligands'; break;
  335. case 'spheres': query = Queries.internal.spheres(); label = 'Coarse Spheres'; break;
  336. default: throw new Error(`${params.type} is a not valid complex element.`);
  337. }
  338. const result = query(new QueryContext(a.data));
  339. const s = Sel.unionStructure(result);
  340. if (s.elementCount === 0) return StateObject.Null;
  341. return new SO.Molecule.Structure(s, { label, description: structureDesc(s) });
  342. }
  343. });
  344. type CustomModelProperties = typeof CustomModelProperties
  345. const CustomModelProperties = PluginStateTransform.BuiltIn({
  346. name: 'custom-model-properties',
  347. display: { name: 'Custom Model Properties' },
  348. from: SO.Molecule.Model,
  349. to: SO.Molecule.Model,
  350. params: (a, ctx: PluginContext) => {
  351. if (!a) return { properties: PD.MultiSelect([], [], { description: 'A list of property descriptor ids.' }) };
  352. return { properties: ctx.customModelProperties.getSelect(a.data) };
  353. }
  354. })({
  355. apply({ a, params }, ctx: PluginContext) {
  356. return Task.create('Custom Props', async taskCtx => {
  357. await attachProps(a.data, ctx, taskCtx, params.properties);
  358. return new SO.Molecule.Model(a.data, { label: 'Props', description: `${params.properties.length} Selected` });
  359. });
  360. }
  361. });
  362. async function attachProps(model: Model, ctx: PluginContext, taskCtx: RuntimeContext, names: string[]) {
  363. for (const name of names) {
  364. const p = ctx.customModelProperties.get(name);
  365. await p.attach(model).runInContext(taskCtx);
  366. }
  367. }
  368. export { ShapeFromPly }
  369. type ShapeFromPly = typeof ShapeFromPly
  370. const ShapeFromPly = PluginStateTransform.BuiltIn({
  371. name: 'shape-from-ply',
  372. display: { name: 'Shape from PLY', description: 'Create Shape from PLY data' },
  373. from: SO.Format.Ply,
  374. to: SO.Shape.Provider,
  375. params(a) {
  376. return { };
  377. }
  378. })({
  379. apply({ a, params }) {
  380. return Task.create('Create shape from PLY', async ctx => {
  381. const shape = await shapeFromPly(a.data, params).runInContext(ctx)
  382. const props = { label: 'Shape' };
  383. return new SO.Shape.Provider(shape, props);
  384. });
  385. }
  386. });