Browse Source

wip, handle custom custom structure props as on-demand dependencies

Alexander Rose 5 years ago
parent
commit
fb15cc135a
24 changed files with 321 additions and 253 deletions
  1. 107 3
      src/mol-model-props/common/custom-property-registry.ts
  2. 20 51
      src/mol-model-props/computed/accessible-surface-area.ts
  3. 15 18
      src/mol-model-props/computed/accessible-surface-area/shrake-rupley.ts
  4. 2 2
      src/mol-model-props/computed/accessible-surface-area/shrake-rupley/area.ts
  5. 2 1
      src/mol-model-props/computed/accessible-surface-area/shrake-rupley/common.ts
  6. 5 5
      src/mol-model-props/computed/accessible-surface-area/shrake-rupley/radii.ts
  7. 54 44
      src/mol-model-props/computed/secondary-structure.ts
  8. 2 1
      src/mol-plugin/behavior/dynamic/custom-props.ts
  9. 22 0
      src/mol-plugin/behavior/dynamic/custom-props/computed/accessible-surface-area.ts
  10. 7 30
      src/mol-plugin/behavior/dynamic/custom-props/computed/secondary-structure.ts
  11. 3 3
      src/mol-plugin/context.ts
  12. 2 1
      src/mol-plugin/index.ts
  13. 3 4
      src/mol-plugin/state/actions/structure.ts
  14. 0 6
      src/mol-plugin/state/representation/model.ts
  15. 0 16
      src/mol-plugin/state/transforms/helpers.ts
  16. 7 12
      src/mol-plugin/state/transforms/model.ts
  17. 6 0
      src/mol-plugin/state/transforms/representation.ts
  18. 1 1
      src/mol-repr/representation.ts
  19. 5 1
      src/mol-repr/structure/representation/cartoon.ts
  20. 4 4
      src/mol-repr/structure/visual/polymer-trace-mesh.ts
  21. 5 11
      src/mol-repr/structure/visual/util/polymer/trace-iterator.ts
  22. 18 13
      src/mol-theme/color/accessible-surface-area.ts
  23. 15 14
      src/mol-theme/color/secondary-structure.ts
  24. 16 12
      src/mol-theme/theme.ts

+ 107 - 3
src/mol-model-props/common/custom-property-registry.ts

@@ -7,8 +7,9 @@
 
 import { CustomPropertyDescriptor, Model, Structure } from '../../mol-model/structure';
 import { OrderedMap } from 'immutable';
-import { ParamDefinition } from '../../mol-util/param-definition';
-import { Task } from '../../mol-task';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { Task, RuntimeContext } from '../../mol-task';
+import { ValueBox } from '../../mol-util';
 
 export { CustomPropertyRegistry }
 
@@ -25,7 +26,7 @@ class CustomPropertyRegistry<T = never> {
             options.push(v.value.option);
             if (v.value.defaultSelected) selected.push(v.value.option[0]);
         }
-        return ParamDefinition.MultiSelect(selected, options);
+        return PD.MultiSelect(selected, options);
     }
 
     getDefault(object: T) {
@@ -66,4 +67,107 @@ namespace CustomPropertyRegistry {
 
     export type ModelProvider = Provider<Model>
     export type StructureProvider = Provider<Structure>
+}
+
+export { CustomStructureProperty }
+
+namespace CustomStructureProperty {
+    export interface Provider<Params extends PD.Params, Value> {
+        label: string
+        descriptor: CustomPropertyDescriptor
+        getParams: (data: Structure) => Params
+        isApplicable: (data: Structure) => boolean
+        attach: (data: Structure, props?: Partial<PD.Values<Params>>) => Task<void>
+        getValue: (data: Structure) => ValueBox<Value | undefined>
+        setProps: (data: Structure, props: PD.Values<Params>) => void
+    }
+
+    export interface ProviderBuilder<Params extends PD.Params, Value> {
+        label: string
+        defaultParams: Params
+        getParams: (data: Structure) => Params
+        isApplicable: (data: Structure) => boolean
+        compute: (ctx: RuntimeContext, data: Structure, props: PD.Values<Params>) => Promise<Value>
+        descriptor: CustomPropertyDescriptor
+    }
+
+    // TODO currently this always uses .inheritedPropertyData
+    export function createProvider<Params extends PD.Params, Value>(builder: ProviderBuilder<Params, Value>): CustomStructureProperty.Provider<Params, Value> {
+        const get = (data: Structure) => {
+            if (!(builder.descriptor.name in data.inheritedPropertyData)) {
+                (data.inheritedPropertyData[builder.descriptor.name] as CustomStructureProperty.Property<PD.Values<Params>, Value>) = {
+                    props: { ...PD.getDefaultValues(builder.getParams(data)) },
+                    data: ValueBox.create(undefined)
+                }
+            }
+            return data.inheritedPropertyData[builder.descriptor.name] as CustomStructureProperty.Property<PD.Values<Params>, Value>;
+        }
+        const set = (data: Structure, props: PD.Values<Params>, value: Value | undefined) => {
+            const property = get(data);
+            (data.inheritedPropertyData[builder.descriptor.name] as CustomStructureProperty.Property<PD.Values<Params>, Value>) = {
+                props,
+                data: ValueBox.withValue(property.data, value)
+            };
+        }
+
+        return {
+            label: builder.label,
+            descriptor: builder.descriptor,
+            getParams: builder.getParams,
+            isApplicable: builder.isApplicable,
+            attach: (data: Structure, props: Partial<PD.Values<Params>> = {}) => Task.create(`Attach ${builder.label}`, async ctx => {
+                const property = get(data)
+                const p = { ...property.props, ...props }
+                if (property.data.value && PD.areEqual(builder.defaultParams, property.props, p)) return
+                const value = await builder.compute(ctx, data, p)
+                data.customPropertyDescriptors.add(builder.descriptor);
+                set(data, p, value);
+            }),
+            getValue: (data: Structure) => get(data)?.data,
+            setProps: (data: Structure, props: Partial<PD.Values<Params>> = {}) => {
+                const property = get(data)
+                const p = { ...property.props, ...props }
+                if (!PD.areEqual(builder.defaultParams, property.props, p)) {
+                    // this invalidates property.value
+                    set(data, p, undefined)
+                }
+            },
+        }
+    }
+
+    export interface Property<P, V> {
+        readonly props: P
+        readonly data: ValueBox<V | undefined>
+    }
+
+    export class Registry {
+        private providers = OrderedMap<string, Provider<any, any>>().asMutable();
+
+        /** Get params for all applicable property providers */
+        getParams(data: Structure) {
+            const values = this.providers.values();
+            const params: PD.Params = {};
+            while (true) {
+                const v = values.next();
+                if (v.done) break;
+                if (!v.value.isApplicable(data)) continue;
+                params[v.value.descriptor.name] = PD.Group(v.value.getParams(data), { label: v.value.label })
+            }
+            return params
+        }
+
+        get(name: string) {
+            const prop = this.providers.get(name);
+            if (!prop) throw new Error(`Custom property '${name}' is not registered.`);
+            return this.providers.get(name);
+        }
+
+        register(provider: Provider<any, any>) {
+            this.providers.set(provider.descriptor.name, provider);
+        }
+
+        unregister(name: string) {
+            this.providers.delete(name);
+        }
+    }
 }

+ 20 - 51
src/mol-model-props/computed/accessible-surface-area.ts

@@ -2,66 +2,35 @@
  * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { ParamDefinition as PD } from '../../mol-util/param-definition'
 import { ShrakeRupleyComputationParams, AccessibleSurfaceArea } from './accessible-surface-area/shrake-rupley';
 import { Structure, CustomPropertyDescriptor } from '../../mol-model/structure';
-import { Task, RuntimeContext } from '../../mol-task';
-import { idFactory } from '../../mol-util/id-factory';
+import { RuntimeContext } from '../../mol-task';
+import { CustomStructureProperty } from '../common/custom-property-registry';
 
+export const AccessibleSurfaceAreaParams = {
+    ...ShrakeRupleyComputationParams
+}
+export type AccessibleSurfaceAreaParams = typeof AccessibleSurfaceAreaParams
+export type AccessibleSurfaceAreaProps = PD.Values<AccessibleSurfaceAreaParams>
 
-const nextAccessibleSurfaceAreaId = idFactory()
-
-export namespace ComputedAccessibleSurfaceArea {
-    export type Property = {
-        id: number
-        asa: AccessibleSurfaceArea
-    }
-
-    export function get(structure: Structure): Property | undefined {
-        return structure.inheritedPropertyData.__ComputedAccessibleSurfaceArea__;
-    }
-    function set(structure: Structure, prop: Property) {
-        (structure.inheritedPropertyData.__ComputedAccessibleSurfaceArea__ as Property) = prop;
-    }
-
-    export function createAttachTask(params: Partial<AccessibleSurfaceAreaComputationProps> = {}) {
-        return (structure: Structure) => attachTask(structure, params)
-    }
-
-    export function attachTask(structure: Structure, params: Partial<AccessibleSurfaceAreaComputationProps> = {}) {
-        return Task.create('Compute Accessible Surface Area', async ctx => {
-            if (get(structure)) return;
-            return await attachFromCifOrCompute(ctx, structure, params)
-        });
-    }
+export type AccessibleSurfaceAreaValue = AccessibleSurfaceArea
 
-    export const Descriptor = CustomPropertyDescriptor({
+export const AccessibleSurfaceAreaProvider: CustomStructureProperty.Provider<AccessibleSurfaceAreaParams, AccessibleSurfaceAreaValue> = CustomStructureProperty.createProvider({
+    label: 'Accessible Surface Area',
+    descriptor: CustomPropertyDescriptor({
         isStatic: true,
         name: 'molstar_accessible_surface_area',
         // TODO `cifExport` and `symbol`
-    });
-
-    export async function attachFromCifOrCompute(ctx: RuntimeContext, structure: Structure, params: Partial<AccessibleSurfaceAreaComputationProps> = {}) {
-        if (structure.customPropertyDescriptors.has(Descriptor)) return;
-
-        const compAccessibleSurfaceArea = await computeAccessibleSurfaceArea(ctx, structure, params)
-
-        structure.customPropertyDescriptors.add(Descriptor);
-        set(structure, compAccessibleSurfaceArea);
+    }),
+    defaultParams: AccessibleSurfaceAreaParams,
+    getParams: (data: Structure) => AccessibleSurfaceAreaParams,
+    isApplicable: (data: Structure) => true,
+    compute: async (ctx: RuntimeContext, data: Structure, props: Partial<AccessibleSurfaceAreaProps>) => {
+        const p = { ...PD.getDefaultValues(AccessibleSurfaceAreaParams), ...props }
+        return await AccessibleSurfaceArea.compute(data, p).runInContext(ctx)
     }
-}
-
-export const AccessibleSurfaceAreaComputationParams = {
-    ...ShrakeRupleyComputationParams
-}
-export type AccessibleSurfaceAreaComputationParams = typeof AccessibleSurfaceAreaComputationParams
-export type AccessibleSurfaceAreaComputationProps = PD.Values<AccessibleSurfaceAreaComputationParams>
-
-async function computeAccessibleSurfaceArea(ctx: RuntimeContext, structure: Structure, params: Partial<AccessibleSurfaceAreaComputationProps>): Promise<ComputedAccessibleSurfaceArea.Property> {
-    const p = { ...PD.getDefaultValues(AccessibleSurfaceAreaComputationParams), params };
-
-    const asa = await AccessibleSurfaceArea.compute(structure, p).runInContext(ctx);
-    return { id: nextAccessibleSurfaceAreaId(), asa }
-}
+})

+ 15 - 18
src/mol-model-props/computed/accessible-surface-area/shrake-rupley.ts

@@ -15,8 +15,8 @@ import { ShrakeRupleyContext, VdWLookup, MaxAsa, DefaultMaxAsa } from './shrake-
 import { computeArea } from './shrake-rupley/area';
 
 export const ShrakeRupleyComputationParams = {
-    numberOfSpherePoints: PD.Numeric(92, { min: 12, max: 120, step: 1 }, { description: 'number of sphere points to sample per atom: 92 (original paper), 960 (BioJava), 3000 (EPPIC) - see Shrake A, Rupley JA: Environment and exposure to solvent of protein atoms. Lysozyme and insulin. J Mol Biol 1973.' }),
-    probeSize: PD.Numeric(1.4, { min: 0.1, max: 4, step: 0.01 }, { description: 'corresponds to the size of a water molecule: 1.4 (original paper), 1.5 (occassionally used)' }),
+    numberOfSpherePoints: PD.Numeric(92, { min: 12, max: 360, step: 1 }, { description: 'Number of sphere points to sample per atom: 92 (original paper), 960 (BioJava), 3000 (EPPIC) - see Shrake A, Rupley JA: Environment and exposure to solvent of protein atoms. Lysozyme and insulin. J Mol Biol 1973.' }),
+    probeSize: PD.Numeric(1.4, { min: 0.1, max: 4, step: 0.01 }, { description: 'Corresponds to the size of a water molecule: 1.4 (original paper), 1.5 (occassionally used)' }),
     // buriedRasaThreshold: PD.Numeric(0.16, { min: 0.0, max: 1.0 }, { description: 'below this cutoff of relative accessible surface area a residue will be considered buried - see: Rost B, Sander C: Conservation and prediction of solvent accessibility in protein families. Proteins 1994.' }),
     nonPolymer: PD.Boolean(false, { description: 'Include non-polymer atoms as occluders.' })
 }
@@ -26,6 +26,13 @@ export type ShrakeRupleyComputationProps = PD.Values<ShrakeRupleyComputationPara
 // TODO
 // - add back buried and relative asa
 
+export { AccessibleSurfaceArea }
+
+interface AccessibleSurfaceArea {
+    readonly serialResidueIndex: ArrayLike<number>
+    readonly area: ArrayLike<number>
+}
+
 namespace AccessibleSurfaceArea {
     /**
      * Adapts the BioJava implementation by Jose Duarte. That implementation is based on the publication by Shrake, A., and
@@ -44,11 +51,8 @@ namespace AccessibleSurfaceArea {
         assignRadiusForHeavyAtoms(ctx);
         await computeArea(runtime, ctx);
 
-        const { accessibleSurfaceArea, serialResidueIndex } = ctx
-        return {
-            serialResidueIndex,
-            accessibleSurfaceArea
-        };
+        const { area, serialResidueIndex } = ctx
+        return { area, serialResidueIndex };
     }
 
     function initialize(structure: Structure, props: ShrakeRupleyComputationProps): ShrakeRupleyContext {
@@ -59,12 +63,12 @@ namespace AccessibleSurfaceArea {
             structure,
             probeSize,
             nonPolymer,
-            spherePoints: generateSpherePoints(numberOfSpherePoints!),
-            scalingConstant: 4.0 * Math.PI / numberOfSpherePoints!,
+            spherePoints: generateSpherePoints(numberOfSpherePoints),
+            scalingConstant: 4.0 * Math.PI / numberOfSpherePoints,
             maxLookupRadius: 2 * props.probeSize + 2 * VdWLookup[2], // 2x probe size + 2x largest VdW
             atomRadiusType: new Int8Array(elementCount),
             serialResidueIndex: new Int32Array(elementCount),
-            accessibleSurfaceArea: new Float32Array(atomicResidueCount)
+            area: new Float32Array(atomicResidueCount)
         }
     }
 
@@ -97,11 +101,4 @@ namespace AccessibleSurfaceArea {
         const maxAsa = MaxAsa[compId] || DefaultMaxAsa;
         return asa / maxAsa
     }
-}
-
-interface AccessibleSurfaceArea {
-    readonly serialResidueIndex: ArrayLike<number>
-    readonly accessibleSurfaceArea: ArrayLike<number>
-}
-
-export { AccessibleSurfaceArea }
+}

+ 2 - 2
src/mol-model-props/computed/accessible-surface-area/shrake-rupley/area.ts

@@ -41,7 +41,7 @@ function setLocation(l: StructureElement.Location, structure: Structure, serialI
 }
 
 function computeRange(ctx: ShrakeRupleyContext, begin: number, end: number) {
-    const { structure, atomRadiusType, serialResidueIndex, accessibleSurfaceArea, spherePoints, scalingConstant, maxLookupRadius, probeSize } = ctx;
+    const { structure, atomRadiusType, serialResidueIndex, area, spherePoints, scalingConstant, maxLookupRadius, probeSize } = ctx;
     const { x, y, z } = StructureProperties.atom
     const { lookup3d, serialMapping, unitIndexMap } = structure;
     const { cumulativeUnitElementCount } = serialMapping
@@ -98,6 +98,6 @@ function computeRange(ctx: ShrakeRupleyContext, begin: number, end: number) {
             if (accessible) ++accessiblePointCount;
         }
 
-        accessibleSurfaceArea[serialResidueIndex[aI]] += scalingConstant * accessiblePointCount * scale * scale;
+        area[serialResidueIndex[aI]] += scalingConstant * accessiblePointCount * scale * scale;
     }
 }

+ 2 - 1
src/mol-model-props/computed/accessible-surface-area/shrake-rupley/common.ts

@@ -16,7 +16,8 @@ export interface ShrakeRupleyContext {
     maxLookupRadius: number,
     atomRadiusType: Int8Array,
     serialResidueIndex: Int32Array,
-    accessibleSurfaceArea: Float32Array
+    /** Accessible surface area values */
+    area: Float32Array
 }
 
 /** Chothia's amino acid and nucleotide atom vdw radii */

+ 5 - 5
src/mol-model-props/computed/accessible-surface-area/shrake-rupley/radii.ts

@@ -50,9 +50,9 @@ export function assignRadiusForHeavyAtoms(ctx: ShrakeRupleyContext) {
                 continue;
             }
 
-            const residueType = getElementMoleculeType(unit, eI)
-            // skip non-polymer groups
-            if (!ctx.nonPolymer && !isPolymer(residueType)) {
+            const moleculeType = getElementMoleculeType(unit, eI)
+            // skip water and optionally non-polymer groups
+            if (moleculeType === MoleculeType.Water || (!ctx.nonPolymer && !isPolymer(moleculeType))) {
                 atomRadiusType[mj] = VdWLookup[0];
                 serialResidueIndex[mj] = -1
                 continue;
@@ -61,9 +61,9 @@ export function assignRadiusForHeavyAtoms(ctx: ShrakeRupleyContext) {
             const atomId = label_atom_id(l);
             const compId = label_comp_id(l);
 
-            if (isNucleic(residueType)) {
+            if (isNucleic(moleculeType)) {
                 atomRadiusType[mj] = determineRadiusNucl(atomId, element, compId);
-            } else if (residueType === MoleculeType.Protein) {
+            } else if (moleculeType === MoleculeType.Protein) {
                 atomRadiusType[mj] = determineRadiusAmino(atomId, element, compId);
             } else {
                 atomRadiusType[mj] = handleNonStandardCase(element);

+ 54 - 44
src/mol-model-props/computed/secondary-structure.ts

@@ -5,60 +5,59 @@
  */
 
 import { CustomPropertyDescriptor, Structure } from '../../mol-model/structure';
-import { Task } from '../../mol-task';
-import { DSSPComputationParams, computeUnitDSSP } from './secondary-structure/dssp';
+import { RuntimeContext } from '../../mol-task';
+import { DSSPComputationParams, DSSPComputationProps, computeUnitDSSP } from './secondary-structure/dssp';
 import { SecondaryStructure } from '../../mol-model/structure/model/properties/seconday-structure';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { Unit } from '../../mol-model/structure/structure';
-import { idFactory } from '../../mol-util/id-factory';
+import { CustomStructureProperty } from '../common/custom-property-registry';
 
-const nextSecondaryStructureId = idFactory()
+// TODO get default params based on structure
+// /**
+//  * Attaches ComputedSecondaryStructure property when unavailable in sourceData and
+//  * when not an archival file (i.e. no database_2.database_id field)
+//  */
+// export async function ensureSecondaryStructure(s: Structure) {
+//     if (s.models.length === 1 && s.model && s.model.sourceData.kind === 'mmCIF') {
+//         if (!s.model.sourceData.data.struct_conf.id.isDefined && !s.model.sourceData.data.struct_sheet_range.id.isDefined &&
+//             !s.model.sourceData.data.database_2.database_id.isDefined
+//         ) {
+//             await ComputedSecondaryStructure.attach(s)
+//         }
+//     }
+// }
 
-export namespace ComputedSecondaryStructure {
-    export type Property = {
-        id: number
-        map: Map<number, SecondaryStructure>
-    }
-
-    export function get(structure: Structure): Property | undefined {
-        return structure.inheritedPropertyData.__ComputedSecondaryStructure__;
-    }
-    function set(structure: Structure, prop: Property) {
-        (structure.inheritedPropertyData.__ComputedSecondaryStructure__ as Property) = prop;
-    }
+export const SecondaryStructureParams = {
+    type: PD.MappedStatic('mmcif', {
+        'mmcif': PD.EmptyGroup({ label: 'mmCIF' }),
+        'dssp': PD.Group(DSSPComputationParams, { label: 'DSSP', isFlat: true })
+    }, { options: [['mmcif', 'mmCIF'], ['dssp', 'DSSP']] })
+}
+export type SecondaryStructureParams = typeof SecondaryStructureParams
+export type SecondaryStructureProps = PD.Values<SecondaryStructureParams>
 
-    export function createAttachTask(params: Partial<SecondaryStructureComputationProps> = {}) {
-        return (structure: Structure) => Task.create('Compute Secondary Structure', async ctx => {
-            if (get(structure)) return true;
-            return await attachFromCifOrCompute(structure, params)
-        });
-    }
+export type SecondaryStructureValue = Map<number, SecondaryStructure>
 
-    export const Descriptor = CustomPropertyDescriptor({
+export const SecondaryStructureProvider: CustomStructureProperty.Provider<SecondaryStructureParams, SecondaryStructureValue> = CustomStructureProperty.createProvider({
+    label: 'Secondary Structure',
+    descriptor: CustomPropertyDescriptor({
         isStatic: true,
         name: 'molstar_computed_secondary_structure',
         // TODO `cifExport` and `symbol`
-    });
-
-    export async function attachFromCifOrCompute(structure: Structure, params: Partial<SecondaryStructureComputationProps> = {}) {
-        if (structure.customPropertyDescriptors.has(Descriptor)) return true;
-
-        const compSecStruc = await computeSecondaryStructure(structure, params)
-
-        structure.customPropertyDescriptors.add(Descriptor);
-        set(structure, compSecStruc);
-        return true;
+    }),
+    defaultParams: SecondaryStructureParams,
+    getParams: (data: Structure) => SecondaryStructureParams,
+    isApplicable: (data: Structure) => true,
+    compute: async (ctx: RuntimeContext, data: Structure, props: Partial<SecondaryStructureProps>) => {
+        const p = { ...PD.getDefaultValues(SecondaryStructureParams), ...props }
+        switch (p.type.name) {
+            case 'dssp': return await computeDssp(data, p.type.params)
+            case 'mmcif': return await computeMmcif(data)
+        }
     }
-}
+})
 
-export const SecondaryStructureComputationParams = {
-    ...DSSPComputationParams
-}
-export type SecondaryStructureComputationParams = typeof SecondaryStructureComputationParams
-export type SecondaryStructureComputationProps = PD.Values<SecondaryStructureComputationParams>
-
-async function computeSecondaryStructure(structure: Structure, params: Partial<SecondaryStructureComputationProps>): Promise<ComputedSecondaryStructure.Property> {
-    const p = { ...PD.getDefaultValues(SecondaryStructureComputationParams), params }
+async function computeDssp(structure: Structure, props: DSSPComputationProps): Promise<SecondaryStructureValue> {
     // TODO take inter-unit hbonds into account for bridge, ladder, sheet assignment
     // TODO store unit-only secStruc as custom unit property???
     // TODO use Zhang-Skolnik for CA alpha only parts or for coarse parts with per-residue elements
@@ -66,9 +65,20 @@ async function computeSecondaryStructure(structure: Structure, params: Partial<S
     for (let i = 0, il = structure.unitSymmetryGroups.length; i < il; ++i) {
         const u = structure.unitSymmetryGroups[i].units[0]
         if (Unit.isAtomic(u)) {
-            const secondaryStructure = await computeUnitDSSP(u, p)
+            const secondaryStructure = await computeUnitDSSP(u, props)
             map.set(u.invariantId, secondaryStructure)
         }
     }
-    return { id: nextSecondaryStructureId(), map }
+    return map
+}
+
+async function computeMmcif(structure: Structure): Promise<SecondaryStructureValue> {
+    const map = new Map<number, SecondaryStructure>()
+    for (let i = 0, il = structure.unitSymmetryGroups.length; i < il; ++i) {
+        const u = structure.unitSymmetryGroups[i].units[0]
+        if (Unit.isAtomic(u)) {
+            map.set(u.invariantId, u.model.properties.secondaryStructure)
+        }
+    }
+    return map
 }

+ 2 - 1
src/mol-plugin/behavior/dynamic/custom-props.ts

@@ -5,6 +5,7 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-export { MolstarSecondaryStructure } from './custom-props/computed/secondary-structure'
+export { AccessibleSurfaceArea } from './custom-props/computed/accessible-surface-area'
+export { SecondaryStructure } from './custom-props/computed/secondary-structure'
 export { PDBeStructureQualityReport } from './custom-props/pdbe/structure-quality-report'
 export { RCSBAssemblySymmetry } from './custom-props/rcsb/assembly-symmetry'

+ 22 - 0
src/mol-plugin/behavior/dynamic/custom-props/computed/accessible-surface-area.ts

@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { PluginBehavior } from '../../../behavior';
+import { AccessibleSurfaceAreaProvider } from '../../../../../mol-model-props/computed/accessible-surface-area';
+
+export const AccessibleSurfaceArea = PluginBehavior.create<{ autoAttach: boolean }>({
+    name: 'computed-accessible-surface-area-prop',
+    category: 'custom-props',
+    display: { name: 'Accessible Surface Area' },
+    ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
+        register(): void {
+            this.ctx.customStructureProperties.register(AccessibleSurfaceAreaProvider);
+        }
+        unregister() {
+            this.ctx.customStructureProperties.unregister(AccessibleSurfaceAreaProvider.descriptor.name);
+        }
+    }
+});

+ 7 - 30
src/mol-plugin/behavior/dynamic/custom-props/computed/secondary-structure.ts

@@ -4,42 +4,19 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { ParamDefinition as PD } from '../../../../../mol-util/param-definition';
 import { PluginBehavior } from '../../../behavior';
-import { CustomPropertyRegistry } from '../../../../../mol-model-props/common/custom-property-registry';
-import { ComputedSecondaryStructure } from '../../../../../mol-model-props/computed/secondary-structure';
+import { SecondaryStructureProvider } from '../../../../../mol-model-props/computed/secondary-structure';
 
-export const MolstarSecondaryStructure = PluginBehavior.create<{ autoAttach: boolean }>({
-    name: 'molstar-computed-secondary-structure-prop',
+export const SecondaryStructure = PluginBehavior.create<{ autoAttach: boolean }>({
+    name: 'computed-secondary-structure-prop',
     category: 'custom-props',
-    display: { name: 'Computed Secondary Structure' },
+    display: { name: 'Secondary Structure' },
     ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
-        private attach = ComputedSecondaryStructure.createAttachTask();
-
-        private provider: CustomPropertyRegistry.StructureProvider = {
-            option: [ComputedSecondaryStructure.Descriptor.name, 'Computed Secondary Structure'],
-            descriptor: ComputedSecondaryStructure.Descriptor,
-            defaultSelected: this.params.autoAttach,
-            attachableTo: () => true,
-            attach: this.attach
-        }
-
         register(): void {
-            this.ctx.customStructureProperties.register(this.provider);
+            this.ctx.customStructureProperties.register(SecondaryStructureProvider);
         }
-
-        update(p: { autoAttach: boolean }) {
-            let updated = this.params.autoAttach !== p.autoAttach
-            this.params.autoAttach = p.autoAttach;
-            this.provider.defaultSelected = p.autoAttach;
-            return updated;
-        }
-
         unregister() {
-            this.ctx.customStructureProperties.unregister(ComputedSecondaryStructure.Descriptor.name);
+            this.ctx.customStructureProperties.unregister(SecondaryStructureProvider.descriptor.name);
         }
-    },
-    params: () => ({
-        autoAttach: PD.Boolean(false)
-    })
+    }
 });

+ 3 - 3
src/mol-plugin/context.ts

@@ -7,7 +7,7 @@
 
 import { List } from 'immutable';
 import { Canvas3D } from '../mol-canvas3d/canvas3d';
-import { CustomPropertyRegistry } from '../mol-model-props/common/custom-property-registry';
+import { CustomPropertyRegistry, CustomStructureProperty } from '../mol-model-props/common/custom-property-registry';
 import { StructureRepresentationRegistry } from '../mol-repr/structure/registry';
 import { VolumeRepresentationRegistry } from '../mol-repr/volume/registry';
 import { State, StateTransform, StateTransformer } from '../mol-state';
@@ -35,7 +35,7 @@ import { StructureElementSelectionManager } from './util/structure-element-selec
 import { SubstructureParentHelper } from './util/substructure-parent-helper';
 import { ModifiersKeys } from '../mol-util/input/input-observer';
 import { isProductionMode, isDebugMode } from '../mol-util/debug';
-import { Model, Structure } from '../mol-model/structure';
+import { Model } from '../mol-model/structure';
 import { Interactivity } from './util/interactivity';
 import { StructureRepresentationHelper } from './util/structure-representation-helper';
 import { StructureSelectionHelper } from './util/structure-selection-helper';
@@ -129,7 +129,7 @@ export class PluginContext {
     } as const
 
     readonly customModelProperties = new CustomPropertyRegistry<Model>();
-    readonly customStructureProperties = new CustomPropertyRegistry<Structure>();
+    readonly customStructureProperties = new CustomStructureProperty.Registry();
     readonly customParamEditors = new Map<string, StateTransformParameters.Class>();
 
     readonly helpers = {

+ 2 - 1
src/mol-plugin/index.ts

@@ -68,7 +68,8 @@ export const DefaultPluginSpec: PluginSpec = {
         PluginSpec.Behavior(PluginBehaviors.Representation.SelectLoci),
         PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider),
         PluginSpec.Behavior(PluginBehaviors.Camera.FocusLoci),
-        PluginSpec.Behavior(PluginBehaviors.CustomProps.MolstarSecondaryStructure, { autoAttach: true }),
+        PluginSpec.Behavior(PluginBehaviors.CustomProps.AccessibleSurfaceArea),
+        PluginSpec.Behavior(PluginBehaviors.CustomProps.SecondaryStructure),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: true, showTooltip: true }),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.RCSBAssemblySymmetry, { autoAttach: true }),
         PluginSpec.Behavior(StructureRepresentationInteraction)

+ 3 - 4
src/mol-plugin/state/actions/structure.ts

@@ -15,7 +15,7 @@ import { CustomModelProperties, StructureSelectionFromExpression, CustomStructur
 import { DataFormatProvider, guessCifVariant, DataFormatBuilderOptions } from './data-format';
 import { FileInfo } from '../../../mol-util/file-info';
 import { Task } from '../../../mol-task';
-import { StructureElement } from '../../../mol-model/structure';
+import { StructureElement, Structure } from '../../../mol-model/structure';
 import { createDefaultStructureComplex } from '../../util/structure-complex-helper';
 import { ModelStructureRepresentation } from '../representation/model';
 
@@ -357,11 +357,10 @@ export const EnableModelCustomProps = StateAction.build({
 });
 
 export const EnableStructureCustomProps = StateAction.build({
-    display: { name: 'Custom Structure Properties', description: 'Enable the addition of custom properties to the structure.' },
+    display: { name: 'Custom Structure Properties', description: 'Enable parameters for custom properties of the structure.' },
     from: PluginStateObject.Molecule.Structure,
     params(a, ctx: PluginContext) {
-        if (!a) return { properties: PD.MultiSelect([], [], { description: 'A list of structure property descriptor ids.' }) };
-        return { properties: ctx.customStructureProperties.getSelect(a.data) };
+        return ctx.customStructureProperties.getParams(a?.data || Structure.Empty)
     },
     isApplicable(a, t, ctx: PluginContext) {
         return t.transformer !== CustomStructureProperties;

+ 0 - 6
src/mol-plugin/state/representation/model.ts

@@ -9,7 +9,6 @@ import { stringToWords } from '../../../mol-util/string';
 import { SpacegroupCell } from '../../../mol-math/geometry';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { Vec3 } from '../../../mol-math/linear-algebra';
-import { ensureSecondaryStructure } from '../transforms/helpers';
 import { RuntimeContext } from '../../../mol-task';
 import { PluginContext } from '../../context';
 import { Assembly, ModelSymmetry } from '../../../mol-model/structure/model/properties/symmetry';
@@ -77,14 +76,12 @@ export namespace ModelStructureRepresentation {
 
         const base = Structure.ofModel(model);
         if (!asm) {
-            await ensureSecondaryStructure(base)
             const label = { label: 'Deposited', description: Structure.elementDescription(base) };
             return new SO.Molecule.Structure(base, label);
         }
 
         id = asm.id;
         const s = await StructureSymmetry.buildAssembly(base, id!).runInContext(ctx);
-        await ensureSecondaryStructure(s)
         const props = { label: `Assembly ${id}`, description: Structure.elementDescription(s) };
         return new SO.Molecule.Structure(s, props);
     }
@@ -92,7 +89,6 @@ export namespace ModelStructureRepresentation {
     async function buildSymmetry(ctx: RuntimeContext, model: Model, ijkMin: Vec3, ijkMax: Vec3) {
         const base = Structure.ofModel(model);
         const s = await StructureSymmetry.buildSymmetryRange(base, ijkMin, ijkMax).runInContext(ctx);
-        await ensureSecondaryStructure(s)
         const props = { label: `Symmetry [${ijkMin}] to [${ijkMax}]`, description: Structure.elementDescription(s) };
         return new SO.Molecule.Structure(s, props);
     }
@@ -100,7 +96,6 @@ export namespace ModelStructureRepresentation {
     async function buildSymmetryMates(ctx: RuntimeContext, model: Model, radius: number) {
         const base = Structure.ofModel(model);
         const s = await StructureSymmetry.builderSymmetryMates(base, radius).runInContext(ctx);
-        await ensureSecondaryStructure(s)
         const props = { label: `Symmetry Mates`, description: Structure.elementDescription(s) };
         return new SO.Molecule.Structure(s, props);
     }
@@ -108,7 +103,6 @@ export namespace ModelStructureRepresentation {
     export async function create(plugin: PluginContext, ctx: RuntimeContext, model: Model, params?: Params): Promise<SO.Molecule.Structure> {
         if (!params || params.name === 'deposited') {
             const s = Structure.ofModel(model);
-            await ensureSecondaryStructure(s);
             return new SO.Molecule.Structure(s, { label: 'Deposited', description: Structure.elementDescription(s) });
         }
         if (params.name === 'assembly') {

+ 0 - 16
src/mol-plugin/state/transforms/helpers.ts

@@ -4,8 +4,6 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Structure } from '../../../mol-model/structure';
-import { ComputedSecondaryStructure } from '../../../mol-model-props/computed/secondary-structure';
 import { PluginStateObject } from '../objects';
 import { DistanceData } from '../../../mol-repr/shape/loci/distance';
 import { LabelData } from '../../../mol-repr/shape/loci/label';
@@ -13,20 +11,6 @@ import { OrientationData } from '../../../mol-repr/shape/loci/orientation';
 import { AngleData } from '../../../mol-repr/shape/loci/angle';
 import { DihedralData } from '../../../mol-repr/shape/loci/dihedral';
 
-/**
- * Attaches ComputedSecondaryStructure property when unavailable in sourceData and
- * when not an archival file (i.e. no database_2.database_id field)
- */
-export async function ensureSecondaryStructure(s: Structure) {
-    if (s.models.length === 1 && s.model && s.model.sourceData.kind === 'mmCIF') {
-        if (!s.model.sourceData.data.struct_conf.id.isDefined && !s.model.sourceData.data.struct_sheet_range.id.isDefined &&
-            !s.model.sourceData.data.database_2.database_id.isDefined
-        ) {
-            await ComputedSecondaryStructure.attachFromCifOrCompute(s)
-        }
-    }
-}
-
 export function getDistanceDataFromStructureSelections(s: ReadonlyArray<PluginStateObject.Molecule.Structure.SelectionEntry>): DistanceData {
     const lociA = s[0].loci
     const lociB = s[1].loci

+ 7 - 12
src/mol-plugin/state/transforms/model.ts

@@ -656,25 +656,20 @@ const CustomStructureProperties = PluginStateTransform.BuiltIn({
     from: SO.Molecule.Structure,
     to: SO.Molecule.Structure,
     params: (a, ctx: PluginContext) => {
-        if (!a) return { properties: PD.MultiSelect([], [], { description: 'A list of property descriptor ids.' }) };
-        return { properties: ctx.customStructureProperties.getSelect(a.data) };
+        return ctx.customStructureProperties.getParams(a?.data || Structure.Empty)
     }
 })({
     apply({ a, params }, ctx: PluginContext) {
         return Task.create('Custom Props', async taskCtx => {
-            await attachStructureProps(a.data, ctx, taskCtx, params.properties);
-            return new SO.Molecule.Structure(a.data, { label: 'Structure Props', description: `${params.properties.length} Selected` });
+            await attachStructureProps(a.data, ctx, taskCtx, params);
+            return new SO.Molecule.Structure(a.data, { label: 'Structure Props' });
         });
     }
 });
-async function attachStructureProps(structure: Structure, ctx: PluginContext, taskCtx: RuntimeContext, names: string[]) {
-    for (const name of names) {
-        try {
-            const p = ctx.customStructureProperties.get(name);
-            await p.attach(structure).runInContext(taskCtx);
-        } catch (e) {
-            ctx.log.warn(`Error attaching structure prop '${name}': ${e}`);
-        }
+async function attachStructureProps(structure: Structure, ctx: PluginContext, taskCtx: RuntimeContext, params: {}) {
+    for (const name of Object.keys(params)) {
+        const property = ctx.customStructureProperties.get(name)
+        property.setProps(structure, params[name as keyof typeof params])
     }
 }
 

+ 6 - 0
src/mol-plugin/state/transforms/representation.ts

@@ -205,8 +205,10 @@ const StructureRepresentation3D = PluginStateTransform.BuiltIn({
     apply({ a, params }, plugin: PluginContext) {
         return Task.create('Structure Representation', async ctx => {
             const provider = plugin.structureRepresentation.registry.get(params.type.name)
+            if (provider.ensureDependencies) await provider.ensureDependencies(a.data).runInContext(ctx)
             const props = params.type.params || {}
             const repr = provider.factory({ webgl: plugin.canvas3d?.webgl, ...plugin.structureRepresentation.themeCtx }, provider.getParams)
+            await Theme.ensureDependencies(plugin.structureRepresentation.themeCtx, { structure: a.data }, params).runInContext(ctx)
             repr.setTheme(Theme.create(plugin.structureRepresentation.themeCtx, { structure: a.data }, params))
             // TODO set initial state, repr.setState({})
             await repr.createOrUpdate(props, a.data).runInContext(ctx);
@@ -216,7 +218,10 @@ const StructureRepresentation3D = PluginStateTransform.BuiltIn({
     update({ a, b, oldParams, newParams }, plugin: PluginContext) {
         return Task.create('Structure Representation', async ctx => {
             if (newParams.type.name !== oldParams.type.name) return StateTransformer.UpdateResult.Recreate;
+            const provider = plugin.structureRepresentation.registry.get(newParams.type.name)
+            if (provider.ensureDependencies) await provider.ensureDependencies(a.data).runInContext(ctx)
             const props = { ...b.data.repr.props, ...newParams.type.params }
+            await Theme.ensureDependencies(plugin.structureRepresentation.themeCtx, { structure: a.data }, newParams).runInContext(ctx)
             b.data.repr.setTheme(Theme.create(plugin.structureRepresentation.themeCtx, { structure: a.data }, newParams));
             await b.data.repr.createOrUpdate(props, a.data).runInContext(ctx);
             b.data.source = a
@@ -575,6 +580,7 @@ const VolumeRepresentation3D = PluginStateTransform.BuiltIn({
     apply({ a, params }, plugin: PluginContext) {
         return Task.create('Volume Representation', async ctx => {
             const provider = plugin.volumeRepresentation.registry.get(params.type.name)
+            if (provider.ensureDependencies) await provider.ensureDependencies(a.data)
             const props = params.type.params || {}
             const repr = provider.factory({ webgl: plugin.canvas3d?.webgl, ...plugin.volumeRepresentation.themeCtx }, provider.getParams)
             repr.setTheme(Theme.create(plugin.volumeRepresentation.themeCtx, { volume: a.data }, params))

+ 1 - 1
src/mol-repr/representation.ts

@@ -47,7 +47,7 @@ export interface RepresentationProvider<D, P extends PD.Params, S extends Repres
     readonly defaultColorTheme: string
     readonly defaultSizeTheme: string
     readonly isApplicable: (data: D) => boolean
-    readonly ensureDependencies?: (data: D) => Promise<void>
+    readonly ensureDependencies?: (data: D) => Task<void>
 }
 
 export namespace RepresentationProvider {

+ 5 - 1
src/mol-repr/structure/representation/cartoon.ts

@@ -15,6 +15,7 @@ import { NucleotideRingParams, NucleotideRingVisual } from '../visual/nucleotide
 import { PolymerDirectionParams, PolymerDirectionVisual } from '../visual/polymer-direction-wedge';
 import { PolymerGapParams, PolymerGapVisual } from '../visual/polymer-gap-cylinder';
 import { PolymerTraceParams, PolymerTraceVisual } from '../visual/polymer-trace-mesh';
+import { SecondaryStructureProvider } from '../../../mol-model-props/computed/secondary-structure';
 
 const CartoonVisuals = {
     'polymer-trace': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, PolymerTraceParams>) => UnitsRepresentation('Polymer trace mesh', ctx, getParams, PolymerTraceVisual),
@@ -61,5 +62,8 @@ export const CartoonRepresentationProvider: StructureRepresentationProvider<Cart
     defaultValues: PD.getDefaultValues(CartoonParams),
     defaultColorTheme: 'polymer-id',
     defaultSizeTheme: 'uniform',
-    isApplicable: (structure: Structure) => structure.polymerResidueCount > 0
+    isApplicable: (structure: Structure) => structure.polymerResidueCount > 0,
+    ensureDependencies: (structure: Structure) => {
+        return SecondaryStructureProvider.attach(structure.root)
+    }
 }

+ 4 - 4
src/mol-repr/structure/visual/polymer-trace-mesh.ts

@@ -16,7 +16,7 @@ import { addSheet } from '../../../mol-geo/geometry/mesh/builder/sheet';
 import { addTube } from '../../../mol-geo/geometry/mesh/builder/tube';
 import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, StructureGroup } from '../units-visual';
 import { VisualUpdateState } from '../../util';
-import { ComputedSecondaryStructure } from '../../../mol-model-props/computed/secondary-structure';
+import { SecondaryStructureProvider } from '../../../mol-model-props/computed/secondary-structure';
 import { addRibbon } from '../../../mol-geo/geometry/mesh/builder/ribbon';
 import { addSphere } from '../../../mol-geo/geometry/mesh/builder/sphere';
 import { Vec3 } from '../../../mol-math/linear-algebra';
@@ -172,10 +172,10 @@ export function PolymerTraceVisual(materialId: number): UnitsVisual<PolymerTrace
                 newProps.arrowFactor !== currentProps.arrowFactor
             )
 
-            const computedSecondaryStructure = ComputedSecondaryStructure.get(newStructureGroup.structure)
-            if ((state.info.computedSecondaryStructure as ComputedSecondaryStructure.Property) !== computedSecondaryStructure) {
+            const secondaryStructureHash = SecondaryStructureProvider.getValue(newStructureGroup.structure).version
+            if ((state.info.secondaryStructureHash as number) !== secondaryStructureHash) {
                 state.createGeometry = true;
-                state.info.computedSecondaryStructure = computedSecondaryStructure
+                state.info.secondaryStructureHash = secondaryStructureHash
             }
         }
     }, materialId)

+ 5 - 11
src/mol-repr/structure/visual/util/polymer/trace-iterator.ts

@@ -13,7 +13,7 @@ import SortedRanges from '../../../../../mol-data/int/sorted-ranges';
 import { CoarseSphereConformation, CoarseGaussianConformation } from '../../../../../mol-model/structure/model/properties/coarse';
 import { getPolymerRanges } from '../polymer';
 import { AtomicConformation } from '../../../../../mol-model/structure/model/properties/atomic';
-import { ComputedSecondaryStructure } from '../../../../../mol-model-props/computed/secondary-structure';
+import { SecondaryStructureProvider } from '../../../../../mol-model-props/computed/secondary-structure';
 import { SecondaryStructure } from '../../../../../mol-model/structure/model/properties/seconday-structure';
 
 /**
@@ -305,16 +305,10 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
         this.value = createPolymerTraceElement(unit)
         this.hasNext = this.residueIt.hasNext && this.polymerIt.hasNext
 
-        this.secondaryStructureType = unit.model.properties.secondaryStructure.type
-        this.secondaryStructureGetIndex = unit.model.properties.secondaryStructure.getIndex
-        const computedSecondaryStructure = ComputedSecondaryStructure.get(structure)
-        if (computedSecondaryStructure) {
-            const secondaryStructure = computedSecondaryStructure.map.get(unit.invariantId)
-            if (secondaryStructure) {
-                this.secondaryStructureType = secondaryStructure.type
-                this.secondaryStructureGetIndex = secondaryStructure.getIndex
-            }
-        }
+        const secondaryStructure = SecondaryStructureProvider.getValue(structure).value?.get(unit.invariantId)
+        if (!secondaryStructure) throw new Error('missing secondary structure')
+        this.secondaryStructureType = secondaryStructure.type
+        this.secondaryStructureGetIndex = secondaryStructure.getIndex
     }
 }
 

+ 18 - 13
src/mol-theme/color/accessible-surface-area.ts

@@ -9,11 +9,12 @@ import { ThemeDataContext } from '../theme';
 import { ColorTheme, LocationColor } from '../color';
 import { ParamDefinition as PD } from '../../mol-util/param-definition'
 import { ColorScale, Color } from '../../mol-util/color';
-import { Unit, StructureElement } from '../../mol-model/structure';
+import { Unit, StructureElement, StructureProperties } from '../../mol-model/structure';
 import { Location } from '../../mol-model/location';
-import { ComputedAccessibleSurfaceArea } from '../../mol-model-props/computed/accessible-surface-area';
+import { AccessibleSurfaceAreaProvider } from '../../mol-model-props/computed/accessible-surface-area';
 import { ColorListName, ColorListOptionsScale } from '../../mol-util/color/lists';
-import { VdWLookup } from '../../mol-model-props/computed/accessible-surface-area/shrake-rupley/common';
+import { Task } from '../../mol-task';
+import { AccessibleSurfaceArea } from '../../mol-model-props/computed/accessible-surface-area/shrake-rupley';
 
 const DefaultColor = Color(0xFAFAFA)
 const Description = 'Assigns a color based on the relative accessible surface area of a residue.'
@@ -35,19 +36,19 @@ export function AccessibleSurfaceAreaColorTheme(ctx: ThemeDataContext, props: PD
         domain: [0.0, 1.0]
     })
 
-    const accessibleSurfaceArea = ctx.structure ? ComputedAccessibleSurfaceArea.get(ctx.structure.root)?.asa : undefined
+    const { label_comp_id } = StructureProperties.residue
+    const accessibleSurfaceArea = ctx.structure && AccessibleSurfaceAreaProvider.getValue(ctx.structure)
+    // use `accessibleSurfaceArea.id` as context hash, when available
+    const contextHash = accessibleSurfaceArea?.version
 
-    if (accessibleSurfaceArea && ctx.structure) {
-        const { structure } = ctx
-        const { getSerialIndex } = structure.root.serialMapping
-        const { relativeAccessibleSurfaceArea, serialResidueIndex } = accessibleSurfaceArea
+    if (accessibleSurfaceArea && accessibleSurfaceArea.value && ctx.structure) {
+        const { getSerialIndex } = ctx.structure.root.serialMapping
+        const { area, serialResidueIndex } = accessibleSurfaceArea.value
 
         color = (location: Location): Color => {
-            if (StructureElement.Location.is(location)) {
-                if (Unit.isAtomic(location.unit)) {
-                    const rSI = serialResidueIndex[getSerialIndex(location.unit, location.element)]
-                    return rSI === -1 ? DefaultColor : scale.color(relativeAccessibleSurfaceArea[rSI])
-                }
+            if (StructureElement.Location.is(location) && Unit.isAtomic(location.unit)) {
+                const rSI = serialResidueIndex[getSerialIndex(location.unit, location.element)]
+                return rSI === -1 ? DefaultColor : scale.color(AccessibleSurfaceArea.normalize(label_comp_id(location), area[rSI]))
             }
             return DefaultColor
         }
@@ -60,6 +61,7 @@ export function AccessibleSurfaceAreaColorTheme(ctx: ThemeDataContext, props: PD
         granularity: 'group',
         color,
         props,
+        contextHash,
         description: Description,
         legend: scale ? scale.legend : undefined
     }
@@ -71,4 +73,7 @@ export const AccessibleSurfaceAreaColorThemeProvider: ColorTheme.Provider<Access
     getParams: getAccessibleSurfaceAreaColorThemeParams,
     defaultValues: PD.getDefaultValues(AccessibleSurfaceAreaColorThemeParams),
     isApplicable: (ctx: ThemeDataContext) => !!ctx.structure,
+    ensureDependencies: (ctx: ThemeDataContext) => {
+        return ctx.structure ? AccessibleSurfaceAreaProvider.attach(ctx.structure.root) : Task.empty()
+    }
 }

+ 15 - 14
src/mol-theme/color/secondary-structure.ts

@@ -13,8 +13,9 @@ import { getElementMoleculeType } from '../../mol-model/structure/util';
 import { ParamDefinition as PD } from '../../mol-util/param-definition'
 import { ThemeDataContext } from '../theme';
 import { TableLegend } from '../../mol-util/legend';
-import { ComputedSecondaryStructure } from '../../mol-model-props/computed/secondary-structure';
+import { SecondaryStructureProvider, SecondaryStructureValue } from '../../mol-model-props/computed/secondary-structure';
 import { getAdjustedColorMap } from '../../mol-util/color/color';
+import { Task } from '../../mol-task';
 
 // from Jmol http://jmol.sourceforge.net/jscolors/ (shapely)
 const SecondaryStructureColors = ColorMap({
@@ -46,14 +47,11 @@ export function getSecondaryStructureColorThemeParams(ctx: ThemeDataContext) {
     return SecondaryStructureColorThemeParams // TODO return copy
 }
 
-export function secondaryStructureColor(colorMap: SecondaryStructureColors, unit: Unit, element: ElementIndex, computedSecondaryStructure: ComputedSecondaryStructure.Property | undefined): Color {
+export function secondaryStructureColor(colorMap: SecondaryStructureColors, unit: Unit, element: ElementIndex, computedSecondaryStructure?: SecondaryStructureValue): Color {
     let secStrucType = SecondaryStructureType.create(SecondaryStructureType.Flag.None)
-    if (Unit.isAtomic(unit)) {
-        secStrucType = unit.model.properties.secondaryStructure.type[unit.residueIndex[element]]
-        if (computedSecondaryStructure) {
-            const secondaryStructure = computedSecondaryStructure.map.get(unit.invariantId)
-            if (secondaryStructure) secStrucType = secondaryStructure.type[secondaryStructure.getIndex(unit.residueIndex[element])]
-        }
+    if (computedSecondaryStructure && Unit.isAtomic(unit)) {
+        const secondaryStructure = computedSecondaryStructure.get(unit.invariantId)
+        if (secondaryStructure) secStrucType = secondaryStructure.type[secondaryStructure.getIndex(unit.residueIndex[element])]
     }
 
     if (SecondaryStructureType.is(secStrucType, SecondaryStructureType.Flag.Helix)) {
@@ -85,17 +83,17 @@ export function secondaryStructureColor(colorMap: SecondaryStructureColors, unit
 }
 
 export function SecondaryStructureColorTheme(ctx: ThemeDataContext, props: PD.Values<SecondaryStructureColorThemeParams>): ColorTheme<SecondaryStructureColorThemeParams> {
-    const computedSecondaryStructure = ctx.structure && ComputedSecondaryStructure.get(ctx.structure)
-    // use `computedSecondaryStructure.id` as context hash, when available
-    const contextHash = computedSecondaryStructure && computedSecondaryStructure.id
+
+    const computedSecondaryStructure = ctx.structure && SecondaryStructureProvider.getValue(ctx.structure)
+    const contextHash = computedSecondaryStructure && computedSecondaryStructure.version
 
     const colorMap = getAdjustedColorMap(SecondaryStructureColors, props.saturation, props.lightness)
 
     function color(location: Location): Color {
         if (StructureElement.Location.is(location)) {
-            return secondaryStructureColor(colorMap, location.unit, location.element, computedSecondaryStructure)
+            return secondaryStructureColor(colorMap, location.unit, location.element, computedSecondaryStructure?.value)
         } else if (Link.isLocation(location)) {
-            return secondaryStructureColor(colorMap, location.aUnit, location.aUnit.elements[location.aIndex], computedSecondaryStructure)
+            return secondaryStructureColor(colorMap, location.aUnit, location.aUnit.elements[location.aIndex], computedSecondaryStructure?.value)
         }
         return DefaultSecondaryStructureColor
     }
@@ -118,5 +116,8 @@ export const SecondaryStructureColorThemeProvider: ColorTheme.Provider<Secondary
     factory: SecondaryStructureColorTheme,
     getParams: getSecondaryStructureColorThemeParams,
     defaultValues: PD.getDefaultValues(SecondaryStructureColorThemeParams),
-    isApplicable: (ctx: ThemeDataContext) => !!ctx.structure
+    isApplicable: (ctx: ThemeDataContext) => !!ctx.structure,
+    ensureDependencies: (ctx: ThemeDataContext) => {
+        return ctx.structure ? SecondaryStructureProvider.attach(ctx.structure.root) : Task.empty()
+    }
 }

+ 16 - 12
src/mol-theme/theme.ts

@@ -10,6 +10,7 @@ import { Structure } from '../mol-model/structure';
 import { VolumeData } from '../mol-model/volume';
 import { ParamDefinition as PD } from '../mol-util/param-definition';
 import { Shape } from '../mol-model/shape';
+import { Task } from '../mol-task';
 
 export interface ThemeRegistryContext {
     colorThemeRegistry: ColorTheme.Registry
@@ -32,27 +33,29 @@ interface Theme {
 }
 
 namespace Theme {
-type Props = { [k: string]: any }
+    type Props = { [k: string]: any }
 
     export function create(ctx: ThemeRegistryContext, data: ThemeDataContext, props: Props, theme?: Theme) {
         theme = theme || createEmpty()
 
-    const colorProps = props.colorTheme as PD.NamedParams
-    const sizeProps = props.sizeTheme as PD.NamedParams
+        const colorProps = props.colorTheme as PD.NamedParams
+        const sizeProps = props.sizeTheme as PD.NamedParams
 
-    theme.color = ctx.colorThemeRegistry.create(colorProps.name, data, colorProps.params)
-    theme.size = ctx.sizeThemeRegistry.create(sizeProps.name, data, sizeProps.params)
+        theme.color = ctx.colorThemeRegistry.create(colorProps.name, data, colorProps.params)
+        theme.size = ctx.sizeThemeRegistry.create(sizeProps.name, data, sizeProps.params)
 
-    return theme
-}
+        return theme
+    }
 
     export function createEmpty(): Theme {
-    return { color: ColorTheme.Empty, size: SizeTheme.Empty }
-}
+        return { color: ColorTheme.Empty, size: SizeTheme.Empty }
+    }
 
-    export async function ensureDependencies(ctx: ThemeRegistryContext, data: ThemeDataContext, props: Props) {
-        await ctx.colorThemeRegistry.get(props.colorTheme.name).ensureDependencies?.(data)
-        await ctx.sizeThemeRegistry.get(props.sizeTheme.name).ensureDependencies?.(data)
+    export function ensureDependencies(ctx: ThemeRegistryContext, data: ThemeDataContext, props: Props) {
+        return Task.create(`Theme dependencies`, async runtime => {
+            await ctx.colorThemeRegistry.get(props.colorTheme.name).ensureDependencies?.(data).runInContext(runtime)
+            await ctx.sizeThemeRegistry.get(props.sizeTheme.name).ensureDependencies?.(data).runInContext(runtime)
+        })
     }
 }
 
@@ -64,6 +67,7 @@ export interface ThemeProvider<T extends ColorTheme<P> | SizeTheme<P>, P extends
     readonly getParams: (ctx: ThemeDataContext) => P
     readonly defaultValues: PD.Values<P>
     readonly isApplicable: (ctx: ThemeDataContext) => boolean
+    readonly ensureDependencies?: (ctx: ThemeDataContext) => Task<void>
 }
 
 function getTypes(list: { name: string, provider: ThemeProvider<any, any> }[]) {