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