From 3be06bb3b649ae47dbbcebe7d3b9aa023694b807 Mon Sep 17 00:00:00 2001 From: Alexander Rose <alexander.rose@weirdbyte.de> Date: Sun, 21 Jul 2019 18:28:45 -0700 Subject: [PATCH] wip, cellpack loader --- src/apps/viewer/extensions/cellpack/data.ts | 57 +++++ src/apps/viewer/extensions/cellpack/model.ts | 209 ++++++++++++++++++ src/apps/viewer/extensions/cellpack/state.ts | 93 ++++++++ src/apps/viewer/extensions/cellpack/util.ts | 47 ++++ src/apps/viewer/index.ts | 10 +- .../structure/structure/structure.ts | 5 +- src/mol-plugin/state/transforms/data.ts | 16 ++ 7 files changed, 434 insertions(+), 3 deletions(-) create mode 100644 src/apps/viewer/extensions/cellpack/data.ts create mode 100644 src/apps/viewer/extensions/cellpack/model.ts create mode 100644 src/apps/viewer/extensions/cellpack/state.ts create mode 100644 src/apps/viewer/extensions/cellpack/util.ts diff --git a/src/apps/viewer/extensions/cellpack/data.ts b/src/apps/viewer/extensions/cellpack/data.ts new file mode 100644 index 000000000..006b38976 --- /dev/null +++ b/src/apps/viewer/extensions/cellpack/data.ts @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { Vec3, Quat } from '../../../../mol-math/linear-algebra'; + +export interface CellPack { + cell: Cell + packings: CellPacking[] +} + +export interface CellPacking { + name: string, + location: 'surface' | 'interior' | 'cytoplasme', + ingredients: Packing['ingredients'] +} + +// + +export interface Cell { + recipe: Recipe + cytoplasme?: Packing + compartments?: { [key: string]: Compartment } +} + +export interface Recipe { + setupfile: string + paths: [string, string][] // [name: string, path: string][] + version: string + name: string +} + +export interface Compartment { + surface?: Packing + interior?: Packing +} + +export interface Packing { + ingredients: { [key: string]: Ingredient } +} + +export interface Ingredient { + source: IngredientSource + results: [Vec3, Quat][] + name: string + positions?: [Vec3[]] // why wrapped in an extra array? + radii?: [number[]] // why wrapped in an extra array? + nbCurve?: number +} + +export interface IngredientSource { + pdb: string + transform: { center: boolean, translate?: Vec3 } + biomt?: boolean +} diff --git a/src/apps/viewer/extensions/cellpack/model.ts b/src/apps/viewer/extensions/cellpack/model.ts new file mode 100644 index 000000000..a192a9955 --- /dev/null +++ b/src/apps/viewer/extensions/cellpack/model.ts @@ -0,0 +1,209 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { StateAction } from '../../../../mol-state'; +import { PluginContext } from '../../../../mol-plugin/context'; +import { PluginStateObject as PSO } from '../../../../mol-plugin/state/objects'; +import { ParamDefinition as PD } from '../../../../mol-util/param-definition'; +import { Ingredient, Packing } from './data'; +import { getFromPdb, getFromCellPackDB } from './util'; +import { Model, Structure, StructureSymmetry, StructureSelection, Queries, QueryContext, StructureProperties as SP } from '../../../../mol-model/structure'; +import { trajectoryFromMmCIF } from '../../../../mol-model-formats/structure/mmcif'; +import { trajectoryFromPDB } from '../../../../mol-model-formats/structure/pdb'; +import { Mat4, Vec3, Quat } from '../../../../mol-math/linear-algebra'; +import { SymmetryOperator } from '../../../../mol-math/geometry'; +import { Task } from '../../../../mol-task'; +import { StructureRepresentation3DHelpers } from '../../../../mol-plugin/state/transforms/representation'; +import { StateTransforms } from '../../../../mol-plugin/state/transforms'; +import { distinctColors } from '../../../../mol-util/color/distinct'; +import { ModelIndexColorThemeProvider } from '../../../../mol-theme/color/model-index'; +import { Hcl } from '../../../../mol-util/color/spaces/hcl'; +import { ParseCellPack, StructureFromCellpack } from './state'; + +function getCellPackModelUrl(fileName: string, baseUrl: string) { + return `${baseUrl}/cellPACK_database_1.1.0/results/${fileName}` +} + +async function getModel(id: string, baseUrl: string) { + let model: Model; + if (id.match(/^[1-9][a-zA-Z0-9]{3,3}$/i)) { + // return + const cif = await getFromPdb(id) + model = (await trajectoryFromMmCIF(cif).run())[0] + } else { + const pdb = await getFromCellPackDB(id, baseUrl) + model = (await trajectoryFromPDB(pdb).run())[0] + } + return model +} + +async function getStructure(model: Model, props: { assembly?: string, trace?: boolean }) { + let structure = Structure.ofModel(model) + const { assembly, trace } = props + + if (assembly) { + structure = await StructureSymmetry.buildAssembly(structure, assembly).run() + } + + if (trace === true) { + const query = Queries.generators.atoms({ atomTest: ctx => { + const atomId = SP.atom.label_atom_id(ctx.element) + return atomId === 'CA' || atomId === 'P' + }}) + const result = query(new QueryContext(structure)) + structure = StructureSelection.unionStructure(result) + } + + const query = Queries.internal.atomicSequence() + const result = query(new QueryContext(structure)) + structure = StructureSelection.unionStructure(result) + + return structure +} + +function getTransform(trans: Vec3, rot: Quat) { + const q: Quat = Quat.create(-rot[3], rot[0], rot[1], rot[2]) + const m: Mat4 = Mat4.fromQuat(Mat4.zero(), q) + Mat4.transpose(m, m) + Mat4.scale(m, m, Vec3.create(-1.0, 1.0, -1.0)) + Mat4.setTranslation(m, trans) + return m +} + +function getTransforms(results: Ingredient['results']) { + return results.map((r: Ingredient['results'][0]) => getTransform(r[0], r[1])) +} + +function getAssembly(transforms: Mat4[], structure: Structure) { + const builder = Structure.Builder(void 0, void 0) + const { units } = structure; + + for (let i = 0, il = transforms.length; i < il; ++i) { + const id = `${i + 1}` + const op = SymmetryOperator.create(id, transforms[i], { id, operList: [ id ] }) + for (const unit of units) { + builder.addWithOperator(unit, op) + } + } + + return builder.getStructure(); +} + +async function getIngredientStructure(ingredient: Ingredient, baseUrl: string) { + const { name, source, results, nbCurve } = ingredient + + // TODO can these be added to the library? + if (name === 'HIV1_CAhex_0_1_0') return + if (name === 'HIV1_CAhexCyclophilA_0_1_0') return + if (name === 'iLDL') return + if (source.pdb === 'None') return + + // TODO handle fibers + if (nbCurve) return + + const model = await getModel(source.pdb || name, baseUrl) + if (!model) return + + const structure = await getStructure(model, { trace: true, assembly: source.biomt ? '1' : undefined }) + const transforms = getTransforms(results) + const assembly = getAssembly(transforms, structure) + return assembly +} + +export function createStructureFromCellPack(ingredients: Packing['ingredients'], baseUrl: string) { + return Task.create('Create Packing Structure', async ctx => { + const structures: Structure[] = [] + for (const iName in ingredients) { + if (ctx.shouldUpdate) ctx.update(iName) + const s = await getIngredientStructure(ingredients[iName], baseUrl) + if (s) structures.push(s) + } + + const builder = Structure.Builder(void 0, void 0) + let offsetInvariantId = 0 + for (const s of structures) { + let maxInvariantId = 0 + for (const u of s.units) { + const invariantId = u.invariantId + offsetInvariantId + if (u.invariantId > maxInvariantId) maxInvariantId = u.invariantId + builder.addUnit(u.kind, u.model, u.conformation.operator, u.elements, invariantId) + } + offsetInvariantId += maxInvariantId + } + + const s = builder.getStructure() + return s + }) +} + +export const LoadCellPackModel = StateAction.build({ + display: { name: 'Load CellPack Model' }, + params: { + id: PD.Select('influenza_model1.json', [ + ['blood_hiv_immature_inside.json', 'blood_hiv_immature_inside'], + ['BloodHIV1.0_mixed_fixed_nc1.cpr', 'BloodHIV1.0_mixed_fixed_nc1'], + ['BloodPlasma1.2.apr.json', 'BloodPlasma1.2'], + ['BloodSerumfillResult.apr', 'BloodSerumfillResult'], + ['HIV-1_0.1.6-8_mixed_radii_pdb.cpr', 'HIV-1_0.1.6-8_mixed_radii_pdb'], + ['influenza_model1.json', 'influenza_model1'], + ['Mycoplasma1.5_mixed_pdb_fixed.cpr', 'Mycoplasma1.5_mixed_pdb_fixed'], + ['NM_Analysis_FigureC1.4.cpr.json', 'NM_Analysis_FigureC1.4'] + ]), + baseUrl: PD.Text('https://cdn.jsdelivr.net/gh/mesoscope/cellPACK_data@master/') + }, + from: PSO.Root +})(({ state, params }, ctx: PluginContext) => Task.create('CellPack Loader', async taskCtx => { + const url = getCellPackModelUrl(params.id, params.baseUrl) + + const root = state.build().toRoot(); + + const cellPackBuilder = root + .apply(StateTransforms.Data.Download, { url, isBinary: false, label: params.id }) + .apply(StateTransforms.Data.ParseJson) + .apply(ParseCellPack) + + const cellPackObject = await state.updateTree(cellPackBuilder).runInContext(taskCtx) + const { packings } = cellPackObject.data + let tree = state.build().to(cellPackBuilder.ref); + + const colors = distinctColors(packings.length) + + for (let i = 0, il = packings.length; i < il; ++i) { + const hcl = Hcl.fromColor(Hcl(), colors[i]) + const hue = [Math.max(0, hcl[0] - 35), Math.min(360, hcl[0] + 35)] as [number, number] + + tree.apply(StructureFromCellpack, { packing: i, baseUrl: params.baseUrl }) + .apply(StateTransforms.Representation.StructureRepresentation3D, + StructureRepresentation3DHelpers.createParams(ctx, Structure.Empty, { + // repr: ctx.structureRepresentation.registry.get('point'), + repr: [ + ctx.structureRepresentation.registry.get('gaussian-surface'), + (c, ctx) => { + return { + quality: 'custom', resolution: 10, radiusOffset: 2, + alpha: 1.0, flatShaded: false, doubleSided: false, + } + } + ], + color: [ + ModelIndexColorThemeProvider, + (c, ctx) => { + return { + palette: { + name: 'generate', + params: { + hue, chroma: [30, 80], luminance: [15, 85], + clusteringStepCount: 50, minSampleCount: 800 + } + } + } + } + ] + })) + } + + await state.updateTree(tree).runInContext(taskCtx); +})); \ No newline at end of file diff --git a/src/apps/viewer/extensions/cellpack/state.ts b/src/apps/viewer/extensions/cellpack/state.ts new file mode 100644 index 000000000..b8b6ce347 --- /dev/null +++ b/src/apps/viewer/extensions/cellpack/state.ts @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { PluginStateObject as PSO, PluginStateTransform } from '../../../../mol-plugin/state/objects'; +import { ParamDefinition as PD } from '../../../../mol-util/param-definition'; +import { Structure } from '../../../../mol-model/structure'; +import { Task } from '../../../../mol-task'; +import { CellPack as _CellPack, Cell, CellPacking } from './data'; +import { createStructureFromCellPack } from './model'; + +export class CellPack extends PSO.Create<_CellPack>({ name: 'CellPack', typeClass: 'Object' }) { } + +export { ParseCellPack } +type ParseCellPack = typeof ParseCellPack +const ParseCellPack = PluginStateTransform.BuiltIn({ + name: 'parse-cellpack', + display: { name: 'Parse CellPack', description: 'Parse CellPack from JSON data' }, + from: PSO.Format.Json, + to: CellPack +})({ + apply({ a }) { + return Task.create('Parse CellPack', async ctx => { + const cell = a.data as Cell + + const packings: CellPacking[] = [] + const { compartments, cytoplasme } = cell + if (compartments) { + for (const name in compartments) { + const { surface, interior } = compartments[name] + if (surface) packings.push({ name, location: 'surface', ingredients: surface.ingredients }) + if (interior) packings.push({ name, location: 'interior', ingredients: interior.ingredients }) + } + } + if (cytoplasme) packings.push({ name: 'Cytoplasme', location: 'cytoplasme', ingredients: cytoplasme.ingredients }) + + return new CellPack({ cell, packings }); + }); + } +}); + +export { StructureFromCellpack } +type StructureFromCellpack = typeof ParseCellPack +const StructureFromCellpack = PluginStateTransform.BuiltIn({ + name: 'structure-from-cellpack', + display: { name: 'Structure from CellPack', description: 'Create Structure from CellPack Packing' }, + from: CellPack, + to: PSO.Molecule.Structure, + params: a => { + if (!a) { + return { + packing: PD.Numeric(0, {}, { description: 'Packing Index' }), + baseUrl: PD.Text('https://cdn.jsdelivr.net/gh/mesoscope/cellPACK_data@master/') + }; + } + const options = a.data.packings.map((d, i) => [i, d.name] as [number, string]) + return { + packing: PD.Select(0, options), + baseUrl: PD.Text('https://cdn.jsdelivr.net/gh/mesoscope/cellPACK_data@master/') + } + } +})({ + apply({ a, params }) { + return Task.create('Structure from CellPack', async ctx => { + const { ingredients, name } = a.data.packings[params.packing] + const structure = await createStructureFromCellPack(ingredients, params.baseUrl).runInContext(ctx) + return new PSO.Molecule.Structure(structure, { label: name }) + }); + } +}); + +export { AddStructure } +type AddStructure = typeof AddStructure +const AddStructure = PluginStateTransform.BuiltIn({ + name: 'add-structure', + display: { name: 'Add Structure', description: 'Add existing molecular structure.' }, + from: PSO.Root, + to: PSO.Molecule.Structure, + params: { + structure: PD.Value<Structure>(Structure.Empty), + label: PD.Text('Structure') + } +})({ + apply({ a, params }) { + return Task.create('Build Structure', async ctx => { + const s = params.structure + const props = { label: params.label, description: s.elementCount === 1 ? '1 element' : `${s.elementCount} elements` }; + return new PSO.Molecule.Structure(s, props); + }) + } +}); \ No newline at end of file diff --git a/src/apps/viewer/extensions/cellpack/util.ts b/src/apps/viewer/extensions/cellpack/util.ts new file mode 100644 index 000000000..2995456bd --- /dev/null +++ b/src/apps/viewer/extensions/cellpack/util.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { CIF } from '../../../../mol-io/reader/cif' +import { parsePDB } from '../../../../mol-io/reader/pdb/parser'; + +async function parseCif(data: string|Uint8Array) { + const comp = CIF.parse(data); + const parsed = await comp.run(); + if (parsed.isError) throw parsed; + return parsed.result; +} + +async function parsePDBfile(data: string) { + const comp = parsePDB(data); + const parsed = await comp.run(); + if (parsed.isError) throw parsed; + return parsed.result; +} + +async function downloadCif(url: string, isBinary: boolean) { + const data = await fetch(url); + return parseCif(isBinary ? new Uint8Array(await data.arrayBuffer()) : await data.text()); +} + +async function downloadPDB(url: string) { + const data = await fetch(url); + return parsePDBfile(await data.text()); +} + +export async function getFromPdb(id: string) { + const parsed = await downloadCif(`https://files.rcsb.org/download/${id}.cif`, false); + return parsed.blocks[0]; +} + +function getCellPackDataUrl(id: string, baseUrl: string) { + const url = `${baseUrl}/cellPACK_database_1.1.0/other/${id}` + return url.endsWith('.pdb') ? url : `${url}.pdb` +} + +export async function getFromCellPackDB(id: string, baseUrl: string) { + const parsed = await downloadPDB(getCellPackDataUrl(id, baseUrl)); + return parsed; +} \ No newline at end of file diff --git a/src/apps/viewer/index.ts b/src/apps/viewer/index.ts index 4a55dab09..9a8bb8cf4 100644 --- a/src/apps/viewer/index.ts +++ b/src/apps/viewer/index.ts @@ -2,6 +2,7 @@ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> + * @author Alexander Rose <alexander.rose@weirdbyte.de> */ import { createPlugin, DefaultPluginSpec } from '../../mol-plugin'; @@ -10,6 +11,8 @@ import { PluginContext } from '../../mol-plugin/context'; import { PluginCommands } from '../../mol-plugin/command'; import { PluginSpec } from '../../mol-plugin/spec'; import { CreateJoleculeState } from './extensions/jolecule'; +import { LoadCellPackModel } from './extensions/cellpack/model'; +import { StructureFromCellpack } from './extensions/cellpack/state'; require('mol-plugin/skin/light.scss') function getParam(name: string, regex: string): string { @@ -21,7 +24,12 @@ const hideControls = getParam('hide-controls', `[^&]+`) === '1'; function init() { const spec: PluginSpec = { - actions: [...DefaultPluginSpec.actions, PluginSpec.Action(CreateJoleculeState)], + actions: [ + ...DefaultPluginSpec.actions, + PluginSpec.Action(CreateJoleculeState), + PluginSpec.Action(LoadCellPackModel), + PluginSpec.Action(StructureFromCellpack), + ], behaviors: [...DefaultPluginSpec.behaviors], animations: [...DefaultPluginSpec.animations || []], customParamEditors: DefaultPluginSpec.customParamEditors, diff --git a/src/mol-model/structure/structure/structure.ts b/src/mol-model/structure/structure/structure.ts index d91b02346..e20803827 100644 --- a/src/mol-model/structure/structure/structure.ts +++ b/src/mol-model/structure/structure/structure.ts @@ -455,8 +455,9 @@ namespace Structure { private units: Unit[] = []; private invariantId = idFactory() - addUnit(kind: Unit.Kind, model: Model, operator: SymmetryOperator, elements: StructureElement.Set): Unit { - const unit = Unit.create(this.units.length, this.invariantId(), kind, model, operator, elements); + addUnit(kind: Unit.Kind, model: Model, operator: SymmetryOperator, elements: StructureElement.Set, invariantId?: number): Unit { + if (invariantId === undefined) invariantId = this.invariantId() + const unit = Unit.create(this.units.length, invariantId, kind, model, operator, elements); this.units.push(unit); return unit; } diff --git a/src/mol-plugin/state/transforms/data.ts b/src/mol-plugin/state/transforms/data.ts index 68d145692..fa24255fa 100644 --- a/src/mol-plugin/state/transforms/data.ts +++ b/src/mol-plugin/state/transforms/data.ts @@ -235,4 +235,20 @@ const ParseDsn6 = PluginStateTransform.BuiltIn({ return new SO.Format.Dsn6(parsed.result); }); } +}); + +export { ParseJson } +type ParseJson = typeof ParseJson +const ParseJson = PluginStateTransform.BuiltIn({ + name: 'parse-json', + display: { name: 'Parse JSON', description: 'Parse JSON from String data' }, + from: [SO.Data.String], + to: SO.Format.Json +})({ + apply({ a }) { + return Task.create('Parse JSON', async ctx => { + const json = await (new Response(a.data)).json(); // async JSON parsing via fetch API + return new SO.Format.Json(json) + }); + } }); \ No newline at end of file -- GitLab