diff --git a/package-lock.json b/package-lock.json
index 5cd5ce9e9f5b8b0c6b8e3a51e9cbd0b25dd54a4d..24f29c7ffd17e68037eca304abde828732480165 100644
Binary files a/package-lock.json and b/package-lock.json differ
diff --git a/package.json b/package.json
index a376d66ec660ecd0a6d414d448da20af92c989c0..6b9d3e8cdacb2caa9d0de392f8eba63daebb36b7 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "molstar",
-  "version": "0.5.1",
+  "version": "0.5.5",
   "description": "A comprehensive macromolecular library.",
   "homepage": "https://github.com/molstar/molstar#readme",
   "repository": {
@@ -72,9 +72,8 @@
     "@graphql-codegen/typescript-graphql-request": "^1.12.2",
     "@graphql-codegen/typescript-operations": "^1.12.2",
     "@types/cors": "^2.8.6",
-    "@typescript-eslint/eslint-plugin": "^2.20.0",
-    "@typescript-eslint/eslint-plugin-tslint": "^2.20.0",
-    "@typescript-eslint/parser": "^2.20.0",
+    "@typescript-eslint/eslint-plugin": "^2.21.0",
+    "@typescript-eslint/parser": "^2.21.0",
     "benchmark": "^2.1.4",
     "circular-dependency-plugin": "^5.2.0",
     "concurrently": "^5.1.0",
@@ -96,7 +95,7 @@
     "simple-git": "^1.131.0",
     "style-loader": "^1.1.3",
     "ts-jest": "^25.2.1",
-    "typescript": "^3.8.2",
+    "typescript": "^3.8.3",
     "webpack": "^4.41.6",
     "webpack-cli": "^3.3.11"
   },
@@ -106,9 +105,9 @@
     "@types/compression": "1.7.0",
     "@types/express": "^4.17.2",
     "@types/jest": "^25.1.3",
-    "@types/node": "^13.7.4",
-    "@types/node-fetch": "^2.5.4",
-    "@types/react": "^16.9.22",
+    "@types/node": "^13.7.7",
+    "@types/node-fetch": "^2.5.5",
+    "@types/react": "^16.9.23",
     "@types/react-dom": "^16.9.5",
     "@types/swagger-ui-dist": "3.0.5",
     "argparse": "^1.0.10",
@@ -117,10 +116,11 @@
     "cors": "^2.8.5",
     "express": "^4.17.1",
     "graphql": "^14.6.0",
+    "immer": "^5.3.6",
     "immutable": "^3.8.2",
     "node-fetch": "^2.6.0",
-    "react": "^16.12.0",
-    "react-dom": "^16.12.0",
+    "react": "^16.13.0",
+    "react-dom": "^16.13.0",
     "rxjs": "^6.5.4",
     "swagger-ui-dist": "^3.25.0",
     "util.promisify": "^1.0.1",
diff --git a/src/apps/basic-wrapper/helpers.ts b/src/apps/basic-wrapper/helpers.ts
index aefcb26e4fd6b4f2fdd7fe211407f549ce82e208..c26cb3eb4b274478620222f333a88f7eea3c05a2 100644
--- a/src/apps/basic-wrapper/helpers.ts
+++ b/src/apps/basic-wrapper/helpers.ts
@@ -67,7 +67,13 @@ export namespace StateHelper {
     }
 
     export function assemble(b: StateBuilder.To<PSO.Molecule.Model>, id?: string) {
-        return b.apply(StateTransforms.Model.StructureAssemblyFromModel, { id: id || 'deposited' }, { tags: 'asm' })
+        const props = {
+            type: {
+                name: 'assembly' as const,
+                params: { id: id || 'deposited' }
+            }
+        }
+        return b.apply(StateTransforms.Model.StructureFromModel, props, { tags: 'asm' })
     }
 
     export function visual(ctx: PluginContext, visualRoot: StateBuilder.To<PSO.Molecule.Structure>) {
diff --git a/src/apps/basic-wrapper/index.ts b/src/apps/basic-wrapper/index.ts
index 79c65460b2a7fb765db156c3f5e0804b2feeb805..1d77c47c9186bf19616a3a6a9cd4c33d317ad62f 100644
--- a/src/apps/basic-wrapper/index.ts
+++ b/src/apps/basic-wrapper/index.ts
@@ -61,10 +61,16 @@ class BasicWrapper {
             ? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif)
             : b.apply(StateTransforms.Model.TrajectoryFromPDB);
 
+        const props = {
+            type: {
+                name: 'assembly' as const,
+                params: { id: assemblyId || 'deposited' }
+            }
+        }
         return parsed
             .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 })
             .apply(StateTransforms.Model.CustomModelProperties, { autoAttach: [StripedResidues.propertyProvider.descriptor.name], properties: {} }, { ref: 'props', state: { isGhost: false } })
-            .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: 'asm' });
+            .apply(StateTransforms.Model.StructureFromModel, props, { ref: 'asm' });
     }
 
     private visual(visualRoot: StateBuilder.To<PSO.Molecule.Structure>) {
@@ -101,8 +107,15 @@ class BasicWrapper {
             tree = state.build();
             this.visual(this.parse(this.download(tree.toRoot(), url), format, assemblyId));
         } else {
+            const props = {
+                type: {
+                    name: 'assembly' as const,
+                    params: { id: assemblyId || 'deposited' }
+                }
+            }
+
             tree = state.build();
-            tree.to('asm').update(StateTransforms.Model.StructureAssemblyFromModel, p => ({ ...p, id: assemblyId || 'deposited' }));
+            tree.to('asm').update(StateTransforms.Model.StructureFromModel, p => ({ ...p, ...props }));
         }
 
         await PluginCommands.State.Update.dispatch(this.plugin, { state: this.plugin.state.dataState, tree });
diff --git a/src/apps/demos/lighting/index.ts b/src/apps/demos/lighting/index.ts
index f54e0eb8eea59ecec5bb3ff27ee87c3a711194e3..dab86f4637c955aee04d9b6ace4e3f36486aaeb0 100644
--- a/src/apps/demos/lighting/index.ts
+++ b/src/apps/demos/lighting/index.ts
@@ -120,9 +120,15 @@ class LightingDemo {
             ? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif)
             : b.apply(StateTransforms.Model.TrajectoryFromPDB);
 
+        const props = {
+            type: {
+                name: 'assembly' as const,
+                params: { id: assemblyId || 'deposited' }
+            }
+        }
         return parsed
             .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 })
-            .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: 'asm' });
+            .apply(StateTransforms.Model.StructureFromModel, props, { ref: 'asm' });
     }
 
     private visual(visualRoot: StateBuilder.To<PSO.Molecule.Structure>) {
@@ -153,8 +159,14 @@ class LightingDemo {
             tree = state.build();
             this.visual(this.parse(this.download(tree.toRoot(), url), format, assemblyId));
         } else {
+            const props = {
+                type: {
+                    name: 'assembly' as const,
+                    params: { id: assemblyId || 'deposited' }
+                }
+            }
             tree = state.build();
-            tree.to('asm').update(StateTransforms.Model.StructureAssemblyFromModel, p => ({ ...p, id: assemblyId || 'deposited' }));
+            tree.to('asm').update(StateTransforms.Model.StructureFromModel, p => ({ ...p, ...props }));
         }
 
         await PluginCommands.State.Update.dispatch(this.plugin, { state: this.plugin.state.dataState, tree });
diff --git a/src/apps/state-docs/pd-to-md.ts b/src/apps/state-docs/pd-to-md.ts
index f0df30519f9e4e18dfd3cd60509d6b45baf244f0..f725105b4028af9d934d5361e1a20198ea15c1ca 100644
--- a/src/apps/state-docs/pd-to-md.ts
+++ b/src/apps/state-docs/pd-to-md.ts
@@ -39,7 +39,7 @@ function paramInfo(param: PD.Any, offset: number): string {
     }
 }
 
-function oToS(options: readonly (readonly [string, string])[]) {
+function oToS(options: readonly (readonly [string, string] | readonly [string, string, string])[]) {
     return options.map(o => `'${o[0]}'`).join(', ');
 }
 
diff --git a/src/apps/structure-info/model.ts b/src/apps/structure-info/model.ts
index 507ef7a19c151e9e7dfbb02c55540c4fbcfd25b5..d0498beff3601b1444116c301229b81fed5da37d 100644
--- a/src/apps/structure-info/model.ts
+++ b/src/apps/structure-info/model.ts
@@ -123,18 +123,6 @@ export function printSequence(model: Model) {
     console.log();
 }
 
-export function printModRes(model: Model) {
-    console.log('\nModified Residues\n=============');
-    const map = model.properties.modifiedResidues.parentId;
-    const { label_comp_id, _rowCount } = model.atomicHierarchy.residues;
-    for (let i = 0; i < _rowCount; i++) {
-        const comp_id = label_comp_id.value(i);
-        if (!map.has(comp_id)) continue;
-        console.log(`[${i}] ${map.get(comp_id)} -> ${comp_id}`);
-    }
-    console.log();
-}
-
 export function printRings(structure: Structure) {
     console.log('\nRings\n=============');
     for (const unit of structure.units) {
@@ -221,7 +209,6 @@ async function run(frame: CifFrame, args: Args) {
     if (args.rings) printRings(structure);
     if (args.intraBonds) printBonds(structure, true, false);
     if (args.interBonds) printBonds(structure, false, true);
-    if (args.mod) printModRes(models[0]);
     if (args.sec) printSecStructure(models[0]);
 }
 
diff --git a/src/examples/proteopedia-wrapper/coloring.ts b/src/examples/proteopedia-wrapper/coloring.ts
index 0f7bc4b3a5f423f5ccabcdb4f5cb38ca521508fb..3b25496365d11cc143d1c182c1e8a273286a171b 100644
--- a/src/examples/proteopedia-wrapper/coloring.ts
+++ b/src/examples/proteopedia-wrapper/coloring.ts
@@ -96,6 +96,7 @@ export function createProteopediaCustomTheme(colors: number[]) {
 
     const ProteopediaCustomColorThemeProvider: ColorTheme.Provider<ProteopediaCustomColorThemeParams> = {
         label: 'Proteopedia Custom',
+        category: 'Custom',
         factory: ProteopediaCustomColorTheme,
         getParams: getChainIdColorThemeParams,
         defaultValues: PD.getDefaultValues(ProteopediaCustomColorThemeParams),
diff --git a/src/examples/proteopedia-wrapper/helpers.ts b/src/examples/proteopedia-wrapper/helpers.ts
index 8611b4ba02d43c5e12770e9935159e4a7520e08f..4efdd23612fc34cc2acb51da55f333be23516716 100644
--- a/src/examples/proteopedia-wrapper/helpers.ts
+++ b/src/examples/proteopedia-wrapper/helpers.ts
@@ -7,7 +7,7 @@
 import { ResidueIndex, Model } from '../../mol-model/structure';
 import { BuiltInStructureRepresentationsName } from '../../mol-repr/structure/registry';
 import { BuiltInColorThemeName } from '../../mol-theme/color';
-import { AminoAcidNames } from '../../mol-model/structure/model/types';
+import { PolymerType } from '../../mol-model/structure/model/types';
 import { PluginContext } from '../../mol-plugin/context';
 import { ModelSymmetry } from '../../mol-model-formats/structure/property/symmetry';
 
@@ -54,15 +54,14 @@ export namespace ModelInfo {
         const hetMap = new Map<string, ModelInfo['hetResidues'][0]>();
 
         for (let rI = 0 as ResidueIndex; rI < residueCount; rI++) {
-            const comp_id = model.atomicHierarchy.residues.label_comp_id.value(rI);
-            if (AminoAcidNames.has(comp_id)) continue;
-            const mod_parent = model.properties.modifiedResidues.parentId.get(comp_id);
-            if (mod_parent && AminoAcidNames.has(mod_parent)) continue;
+            if (model.atomicHierarchy.derived.residue.polymerType[rI] !== PolymerType.NA) continue;
 
             const cI = chainIndex[residueOffsets[rI]];
             const eI = model.atomicHierarchy.index.getEntityFromChain(cI);
             if (model.entities.data.type.value(eI) === 'water') continue;
 
+            const comp_id = model.atomicHierarchy.residues.label_comp_id.value(rI);
+
             let lig = hetMap.get(comp_id);
             if (!lig) {
                 lig = { name: comp_id, indices: [] };
diff --git a/src/examples/proteopedia-wrapper/index.ts b/src/examples/proteopedia-wrapper/index.ts
index 7254ea8b8727546c2c030bd517a0879cec262d2d..2ecd40ac6a54e4d3f43cb5d3ff2785735b06b1bc 100644
--- a/src/examples/proteopedia-wrapper/index.ts
+++ b/src/examples/proteopedia-wrapper/index.ts
@@ -92,10 +92,16 @@ class MolStarProteopediaWrapper {
 
     private structure(assemblyId: string) {
         const model = this.state.build().to(StateElements.Model);
+        const props = {
+            type: {
+                name: 'assembly' as const,
+                params: { id: assemblyId || 'deposited' }
+            }
+        }
 
         const s = model
             .apply(StateTransforms.Model.CustomModelProperties, { autoAttach: [EvolutionaryConservation.propertyProvider.descriptor.name], properties: {} }, { ref: StateElements.ModelProps, state: { isGhost: false } })
-            .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: StateElements.Assembly });
+            .apply(StateTransforms.Model.StructureFromModel, props, { ref: StateElements.Assembly });
 
         s.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' }, { ref: StateElements.Sequence });
         s.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' }, { ref: StateElements.Het });
@@ -213,7 +219,13 @@ class MolStarProteopediaWrapper {
             const tree = state.build();
             const info = await this.doInfo(true);
             const asmId = (assemblyId === 'preferred' && info && info.preferredAssemblyId) || assemblyId;
-            tree.to(StateElements.Assembly).update(StateTransforms.Model.StructureAssemblyFromModel, p => ({ ...p, id: asmId }));
+            const props = {
+                type: {
+                    name: 'assembly' as const,
+                    params: { id: asmId || 'deposited' }
+                }
+            }
+            tree.to(StateElements.Assembly).update(StateTransforms.Model.StructureFromModel, p => ({ ...p, ...props }));
             await this.applyState(tree);
         }
 
diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts
index a00217f4b7b7aed0e509e252a7fd5ffcd6483c66..5d91550849344da4735be84e2ef6ff116d597e65 100644
--- a/src/mol-canvas3d/canvas3d.ts
+++ b/src/mol-canvas3d/canvas3d.ts
@@ -77,7 +77,7 @@ interface Canvas3D {
 
     handleResize(): void
     /** Focuses camera on scene's bounding sphere, centered and zoomed. */
-    requestCameraReset(durationMs?: number): void
+    requestCameraReset(options?: { durationMs?: number, snapshot?: Partial<Camera.Snapshot> }): void
     readonly camera: Camera
     readonly boundingSphere: Readonly<Sphere3D>
     downloadScreenshot(): void
@@ -191,6 +191,7 @@ namespace Canvas3D {
         let drawPending = false
         let cameraResetRequested = false
         let nextCameraResetDuration: number | undefined = void 0
+        let nextCameraResetSnapshot: Partial<Camera.Snapshot> | undefined = void 0
 
         function getLoci(pickingId: PickingId) {
             let loci: Loci = EmptyLoci
@@ -290,9 +291,15 @@ namespace Canvas3D {
         function resolveCameraReset() {
             if (!cameraResetRequested) return;
             const { center, radius } = scene.boundingSphere;
-            const duration = nextCameraResetDuration === undefined ? p.cameraResetDurationMs : nextCameraResetDuration
-            camera.focus(center, radius, radius, duration);
+            if (radius > 0) {
+                const duration = nextCameraResetDuration === undefined ? p.cameraResetDurationMs : nextCameraResetDuration
+                const focus = camera.getFocus(center, radius, radius);
+                const snapshot = nextCameraResetSnapshot ? { ...focus, ...nextCameraResetSnapshot } : focus;
+                camera.setState(snapshot, duration);
+            }
+
             nextCameraResetDuration = void 0;
+            nextCameraResetSnapshot = void 0;
             cameraResetRequested = false;
         }
 
@@ -388,8 +395,9 @@ namespace Canvas3D {
             getLoci,
 
             handleResize,
-            requestCameraReset: (durationMs) => {
-                nextCameraResetDuration = durationMs;
+            requestCameraReset: options => {
+                nextCameraResetDuration = options?.durationMs;
+                nextCameraResetSnapshot = options?.snapshot;
                 cameraResetRequested = true;
             },
             camera,
diff --git a/src/mol-geo/geometry/base.ts b/src/mol-geo/geometry/base.ts
index 5cce347e24baa25b2e09a91c197d8728f68d4834..f726d3447da2dc8c9d01cc144c0e3c452ce1f863 100644
--- a/src/mol-geo/geometry/base.ts
+++ b/src/mol-geo/geometry/base.ts
@@ -28,8 +28,8 @@ export const VisualQualityInfo = {
     'lowest': {},
 }
 export type VisualQuality = keyof typeof VisualQualityInfo
-export const VisualQualityNames = Object.keys(VisualQualityInfo)
-export const VisualQualityOptions = VisualQualityNames.map(n => [n, n] as [VisualQuality, string])
+export const VisualQualityNames = Object.keys(VisualQualityInfo) as VisualQuality[]
+export const VisualQualityOptions = PD.arrayToOptions(VisualQualityNames)
 
 //
 
diff --git a/src/mol-math/geometry/_spec/spacegroup.spec.ts b/src/mol-math/geometry/_spec/spacegroup.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7783f936c638b226e08433d1a85a8fad57a91696
--- /dev/null
+++ b/src/mol-math/geometry/_spec/spacegroup.spec.ts
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Spacegroup, SpacegroupCell } from '../spacegroup/construction';
+import { Vec3 } from '../../linear-algebra';
+
+function getSpacegroup(name: string) {
+    const size = Vec3.create(1, 1, 1)
+    const anglesInRadians = Vec3.create(Math.PI / 2, Math.PI / 2, Math.PI / 2)
+    const cell = SpacegroupCell.create(name, size, anglesInRadians)
+    return Spacegroup.create(cell)
+}
+
+function checkOperatorsXyz(name: string, expected: string[]) {
+    const spacegroup = getSpacegroup(name)
+    for (let i = 0, il = spacegroup.operators.length; i < il; ++i) {
+        const op = spacegroup.operators[i]
+        const actual = Spacegroup.getOperatorXyz(op)
+        expect(actual).toBe(expected[i])
+    }
+}
+
+describe('Spacegroup', () => {
+    it('operators xyz', () => {
+        checkOperatorsXyz('P 1', ['X,Y,Z'])
+        checkOperatorsXyz('P -1', ['X,Y,Z', '-X,-Y,-Z'])
+        checkOperatorsXyz('P 1 21 1', ['X,Y,Z', '-X,1/2+Y,-Z'])
+        checkOperatorsXyz('P 1 21/m 1', ['X,Y,Z', '-X,1/2+Y,-Z', '-X,-Y,-Z', 'X,1/2-Y,Z'])
+        checkOperatorsXyz('P 41', ['X,Y,Z', '-X,-Y,1/2+Z', '-Y,X,1/4+Z', 'Y,-X,3/4+Z'])
+        checkOperatorsXyz('P 41 21 2', ['X,Y,Z', '-X,-Y,1/2+Z', '1/2-Y,1/2+X,1/4+Z', '1/2+Y,1/2-X,3/4+Z', '1/2-X,1/2+Y,1/4-Z', '1/2+X,1/2-Y,3/4-Z', 'Y,X,-Z', '-Y,-X,1/2-Z'])
+        checkOperatorsXyz('P 3', ['X,Y,Z', '-Y,X-Y,Z', 'Y-X,-X,Z'])
+    });
+})
diff --git a/src/mol-math/geometry/spacegroup/construction.ts b/src/mol-math/geometry/spacegroup/construction.ts
index 9bc4d9d6cee4cf8879c43ae978b66cbfa45a9028..944c12cf3352af1d42f0c59472d15bade372cd87 100644
--- a/src/mol-math/geometry/spacegroup/construction.ts
+++ b/src/mol-math/geometry/spacegroup/construction.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 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>
@@ -145,6 +145,54 @@ namespace Spacegroup {
         const r3 = TransformData[ids[2]];
         return Mat4.ofRows([r1, r2, r3, [0, 0, 0, 1]]);
     }
+
+    export function getOperatorXyz(op: Mat4) {
+        return [
+            formatElement(getRotation(op[0], op[4], op[8]), getShift(op[12])),
+            formatElement(getRotation(op[1], op[5], op[9]), getShift(op[13])),
+            formatElement(getRotation(op[2], op[6], op[10]), getShift(op[14]))
+        ].join(',')
+    }
+
+    function getRotation(x: number, y: number, z: number) {
+        let r: string[] = []
+        if (x > 0) r.push('+X')
+        else if (x < 0) r.push('-X')
+        if (y > 0) r.push('+Y')
+        else if (y < 0) r.push('-Y')
+        if (z > 0) r.push('+Z')
+        else if (z < 0) r.push('-Z')
+
+        if (r.length === 1) {
+            return r[0].charAt(0) === '+' ? r[0].substr(1) : r[0]
+        }
+        if (r.length === 2) {
+            const s0 = r[0].charAt(0)
+            const s1 = r[1].charAt(0)
+            if (s0 === '+') return `${r[0].substr(1)}${r[1]}`
+            if (s1 === '+') return `${r[1].substr(1)}${r[0]}`
+        }
+        throw new Error(`unknown rotation '${r}', ${x} ${y} ${z}`)
+    }
+
+    function getShift(s: number) {
+        switch (s) {
+            case 1/2: return '1/2'
+            case 1/4: return '1/4'
+            case 3/4: return '3/4'
+            case 1/3: return '1/3'
+            case 2/3: return '2/3'
+            case 1/6: return '1/6'
+            case 5/6: return '5/6'
+        }
+        return ''
+    }
+
+    function formatElement(rotation: string, shift: string) {
+        if (shift === '') return rotation
+        if (rotation.length > 2) return `${rotation}+${shift}`
+        return rotation.charAt(0) === '-' ? `${shift}${rotation}` : `${shift}+${rotation}`
+    }
 }
 
 export { Spacegroup, SpacegroupCell }
\ No newline at end of file
diff --git a/src/mol-model-formats/structure/basic/parser.ts b/src/mol-model-formats/structure/basic/parser.ts
index cf41cd1866c7c82aef5f6b339cdf6d5f28a8e402..554c6eea02fa352e9ec5962db71e8ab0b42b7d3c 100644
--- a/src/mol-model-formats/structure/basic/parser.ts
+++ b/src/mol-model-formats/structure/basic/parser.ts
@@ -45,7 +45,7 @@ function createStandardModel(data: BasicData, atom_site: AtomSite, sourceIndex:
     }
 
     const coarse = EmptyCoarse;
-    const sequence = getSequence(data, entities, atomic.hierarchy, coarse.hierarchy, properties.modifiedResidues.parentId)
+    const sequence = getSequence(data, entities, atomic.hierarchy, coarse.hierarchy)
     const atomicRanges = getAtomicRanges(atomic.hierarchy, entities, atomic.conformation, sequence)
 
     const entry = data.entry.id.valueKind(0) === Column.ValueKind.Present
@@ -80,7 +80,7 @@ function createStandardModel(data: BasicData, atom_site: AtomSite, sourceIndex:
 function createIntegrativeModel(data: BasicData, ihm: CoarseData, properties: Model['properties'], format: ModelFormat): Model {
     const atomic = getAtomicHierarchyAndConformation(ihm.atom_site, ihm.atom_site_sourceIndex, ihm.entities, properties.chemicalComponentMap);
     const coarse = getCoarse(ihm, properties);
-    const sequence = getSequence(data, ihm.entities, atomic.hierarchy, coarse.hierarchy, properties.modifiedResidues.parentId)
+    const sequence = getSequence(data, ihm.entities, atomic.hierarchy, coarse.hierarchy)
     const atomicRanges = getAtomicRanges(atomic.hierarchy, ihm.entities, atomic.conformation, sequence)
 
     const entry = data.entry.id.valueKind(0) === Column.ValueKind.Present
diff --git a/src/mol-model-formats/structure/basic/properties.ts b/src/mol-model-formats/structure/basic/properties.ts
index 45ecd6e5d17ee6f0d4e13803c91981b6cf0369b0..743c57c7f71b2565896b6a587cfecc9840afcbb0 100644
--- a/src/mol-model-formats/structure/basic/properties.ts
+++ b/src/mol-model-formats/structure/basic/properties.ts
@@ -6,30 +6,13 @@
  */
 
 import { Model } from '../../../mol-model/structure/model/model';
-import { ChemicalComponent, MissingResidue } from '../../../mol-model/structure/model/properties/common';
+import { ChemicalComponent, MissingResidue, StructAsym } from '../../../mol-model/structure/model/properties/common';
 import { getMoleculeType, MoleculeType, getDefaultChemicalComponent } from '../../../mol-model/structure/model/types';
 import { SaccharideComponentMap, SaccharideComponent, SaccharidesSnfgMap, SaccharideCompIdMap, UnknownSaccharideComponent } from '../../../mol-model/structure/structure/carbohydrates/constants';
 import { memoize1 } from '../../../mol-util/memoize';
 import { BasicData } from './schema';
 import { Table } from '../../../mol-data/db';
 
-function getModifiedResidueNameMap(data: BasicData): Model['properties']['modifiedResidues'] {
-    const parentId = new Map<string, string>();
-    const details = new Map<string, string>();
-
-    const c = data.pdbx_struct_mod_residue;
-    const comp_id = c.label_comp_id.isDefined ? c.label_comp_id : c.auth_comp_id;
-    const parent_id = c.parent_comp_id, details_data = c.details;
-
-    for (let i = 0; i < c._rowCount; i++) {
-        const id = comp_id.value(i);
-        parentId.set(id, parent_id.value(i));
-        details.set(id, details_data.value(i));
-    }
-
-    return { parentId, details };
-}
-
 function getMissingResidues(data: BasicData): Model['properties']['missingResidues'] {
     const map = new Map<string, MissingResidue>();
     const getKey = (model_num: number, asym_id: string, seq_id: number) => {
@@ -124,11 +107,43 @@ const getUniqueComponentNames = memoize1((data: BasicData) => {
     return uniqueNames
 })
 
+
+function getStructAsymMap(data: BasicData): Model['properties']['structAsymMap'] {
+    const map = new Map<string, StructAsym>();
+
+    const { label_asym_id, auth_asym_id, label_entity_id } = data.atom_site
+    for (let i = 0, il = label_asym_id.rowCount; i < il; ++i) {
+        const id = label_asym_id.value(i)
+        if (!map.has(id)) {
+            map.set(id, {
+                id,
+                auth_id: auth_asym_id.value(i),
+                entity_id: label_entity_id.value(i)
+            })
+        }
+    }
+
+    if (data.struct_asym._rowCount > 0) {
+        const { id, entity_id } = data.struct_asym
+        for (let i = 0, il = id.rowCount; i < il; ++i) {
+            const _id = id.value(i)
+            if (!map.has(_id)) {
+                map.set(_id, {
+                    id: _id,
+                    auth_id: '',
+                    entity_id: entity_id.value(i)
+                })
+            }
+        }
+    }
+    return map
+}
+
 export function getProperties(data: BasicData): Model['properties'] {
     return {
-        modifiedResidues: getModifiedResidueNameMap(data),
         missingResidues: getMissingResidues(data),
         chemicalComponentMap: getChemicalComponentMap(data),
-        saccharideComponentMap: getSaccharideComponentMap(data)
+        saccharideComponentMap: getSaccharideComponentMap(data),
+        structAsymMap: getStructAsymMap(data)
     }
 }
\ No newline at end of file
diff --git a/src/mol-model-formats/structure/basic/schema.ts b/src/mol-model-formats/structure/basic/schema.ts
index dea7e328e8a9db387542f49ee8c057e7fb9f73aa..670432bc674935ecfb1cb8a1acdf473f4c049417 100644
--- a/src/mol-model-formats/structure/basic/schema.ts
+++ b/src/mol-model-formats/structure/basic/schema.ts
@@ -8,7 +8,6 @@ import { mmCIF_Schema } from '../../../mol-io/reader/cif/schema/mmcif';
 import { Table } from '../../../mol-data/db';
 
 // TODO split into conformation and hierarchy parts
-// TODO extract `pdbx_struct_mod_residue` as property?
 
 export type Entry = Table<mmCIF_Schema['entry']>
 export type Struct = Table<mmCIF_Schema['struct']>
@@ -22,7 +21,6 @@ export type EntityPolySeq = Table<mmCIF_Schema['entity_poly_seq']>
 export type EntityBranch = Table<mmCIF_Schema['pdbx_entity_branch']>
 export type ChemComp = Table<mmCIF_Schema['chem_comp']>
 export type ChemCompIdentifier = Table<mmCIF_Schema['pdbx_chem_comp_identifier']>
-export type StructModResidue = Table<mmCIF_Schema['pdbx_struct_mod_residue']>
 export type AtomSite = Table<mmCIF_Schema['atom_site']>
 export type IhmSphereObjSite = Table<mmCIF_Schema['ihm_sphere_obj_site']>
 export type IhmGaussianObjSite =Table<mmCIF_Schema['ihm_gaussian_obj_site']>
@@ -41,7 +39,6 @@ export const BasicSchema = {
     pdbx_entity_branch: mmCIF_Schema.pdbx_entity_branch,
     chem_comp: mmCIF_Schema.chem_comp,
     pdbx_chem_comp_identifier: mmCIF_Schema.pdbx_chem_comp_identifier,
-    pdbx_struct_mod_residue: mmCIF_Schema.pdbx_struct_mod_residue,
     atom_site: mmCIF_Schema.atom_site,
     ihm_sphere_obj_site: mmCIF_Schema.ihm_sphere_obj_site,
     ihm_gaussian_obj_site: mmCIF_Schema.ihm_gaussian_obj_site,
@@ -61,7 +58,6 @@ export interface BasicData {
     pdbx_entity_branch: EntityBranch
     chem_comp: ChemComp
     pdbx_chem_comp_identifier: ChemCompIdentifier
-    pdbx_struct_mod_residue: StructModResidue
     atom_site: AtomSite
     ihm_sphere_obj_site: IhmSphereObjSite
     ihm_gaussian_obj_site: IhmGaussianObjSite
diff --git a/src/mol-model-formats/structure/basic/sequence.ts b/src/mol-model-formats/structure/basic/sequence.ts
index d73a6f84a50bf48ff02504a0002813012f49d417..8a3328d52ddfdb7e2d55876c72aa52c6fb84ffd8 100644
--- a/src/mol-model-formats/structure/basic/sequence.ts
+++ b/src/mol-model-formats/structure/basic/sequence.ts
@@ -13,9 +13,9 @@ import { Sequence } from '../../../mol-model/sequence';
 import { CoarseHierarchy } from '../../../mol-model/structure/model/properties/coarse';
 import { BasicData } from './schema';
 
-export function getSequence(data: BasicData, entities: Entities, atomicHierarchy: AtomicHierarchy, coarseHierarchy: CoarseHierarchy, modResMap: ReadonlyMap<string, string>): StructureSequence {
+export function getSequence(data: BasicData, entities: Entities, atomicHierarchy: AtomicHierarchy, coarseHierarchy: CoarseHierarchy): StructureSequence {
     if (!data.entity_poly_seq || !data.entity_poly_seq._rowCount) {
-        return StructureSequence.fromHierarchy(entities, atomicHierarchy, coarseHierarchy, modResMap);
+        return StructureSequence.fromHierarchy(entities, atomicHierarchy, coarseHierarchy);
     }
 
     const { entity_id, num, mon_id } = data.entity_poly_seq;
@@ -37,7 +37,7 @@ export function getSequence(data: BasicData, entities: Entities, atomicHierarchy
 
         byEntityKey[entityKey] = {
             entityId: id,
-            sequence: Sequence.ofResidueNames(compId, seqId, modResMap)
+            sequence: Sequence.ofResidueNames(compId, seqId)
         };
 
         sequences.push(byEntityKey[entityKey]);
diff --git a/src/mol-model-formats/structure/common/component.ts b/src/mol-model-formats/structure/common/component.ts
index 483ac6e95369b33506deee7132a663104a8a7b91..7dbbfa176c7bcc7eb0907b04015d539ee67c13ff 100644
--- a/src/mol-model-formats/structure/common/component.ts
+++ b/src/mol-model-formats/structure/common/component.ts
@@ -6,7 +6,7 @@
 
 import { Table, Column } from '../../../mol-data/db';
 import { mmCIF_Schema } from '../../../mol-io/reader/cif/schema/mmcif';
-import { WaterNames } from '../../../mol-model/structure/model/types';
+import { WaterNames, PolymerNames } from '../../../mol-model/structure/model/types';
 import { SetUtils } from '../../../mol-util/set';
 import { BasicSchema } from '../basic/schema';
 
@@ -77,12 +77,14 @@ export class ComponentBuilder {
     private ids: string[] = []
     private names: string[] = []
     private types: mmCIF_Schema['chem_comp']['type']['T'][] = []
+    private mon_nstd_flags: mmCIF_Schema['chem_comp']['mon_nstd_flag']['T'][] = []
 
     private set(c: Component) {
         this.comps.set(c.id, c)
         this.ids.push(c.id)
         this.names.push(c.name)
         this.types.push(c.type)
+        this.mon_nstd_flags.push(PolymerNames.has(c.id) ? 'y' : 'n')
     }
 
     private getAtomIds(index: number) {
@@ -141,6 +143,7 @@ export class ComponentBuilder {
             id: Column.ofStringArray(this.ids),
             name: Column.ofStringArray(this.names),
             type: Column.ofStringAliasArray(this.types),
+            mon_nstd_flag: Column.ofStringAliasArray(this.mon_nstd_flags),
         }, this.ids.length)
     }
 
diff --git a/src/mol-model-formats/structure/common/property.ts b/src/mol-model-formats/structure/common/property.ts
index 44bae5f2f3e0e1642a01a9fc10b3a302dde39f69..873e8bd5848a0bf1edd36a69f9001d4555c293fb 100644
--- a/src/mol-model-formats/structure/common/property.ts
+++ b/src/mol-model-formats/structure/common/property.ts
@@ -14,6 +14,10 @@ class FormatRegistry<T> {
         this.map.set(kind, obtain)
     }
 
+    remove(kind: ModelFormat['kind']) {
+        this.map.delete(kind)
+    }
+
     get(kind: ModelFormat['kind']) {
         return this.map.get(kind)
     }
diff --git a/src/mol-model-formats/structure/mmcif.ts b/src/mol-model-formats/structure/mmcif.ts
index d52f7898ce97b0431e80ea59a6e726efa58e3660..ae7c3deb8147f42d7a2f660493581d921cc68b31 100644
--- a/src/mol-model-formats/structure/mmcif.ts
+++ b/src/mol-model-formats/structure/mmcif.ts
@@ -17,7 +17,6 @@ import { Table } from '../../mol-data/db';
 import { AtomSiteAnisotrop } from './property/anisotropic';
 import { ComponentBond } from './property/bonds/comp';
 import { StructConn } from './property/bonds/struct_conn';
-import { ModelCrossLinkRestraint } from './property/pair-restraints/cross-links';
 
 function modelSymmetryFromMmcif(model: Model) {
     if (!MmcifFormat.is(model.sourceData)) return;
@@ -65,14 +64,6 @@ function structConnFromMmcif(model: Model) {
 }
 StructConn.Provider.formatRegistry.add('mmCIF', structConnFromMmcif)
 
-function crossLinkRestraintFromMmcif(model: Model) {
-    if (!MmcifFormat.is(model.sourceData)) return;
-    const { ihm_cross_link_restraint } = model.sourceData.data.db;
-    if (ihm_cross_link_restraint._rowCount === 0) return;
-    return ModelCrossLinkRestraint.fromTable(ihm_cross_link_restraint, model)
-}
-ModelCrossLinkRestraint.Provider.formatRegistry.add('mmCIF', crossLinkRestraintFromMmcif)
-
 //
 
 export { MmcifFormat }
diff --git a/src/mol-model-formats/structure/pdb/to-cif.ts b/src/mol-model-formats/structure/pdb/to-cif.ts
index 05e0f8592689d7a48a0d44b3cac74e06f83284d5..708ff61e5a57349b16f54777780ad08075cf8f03 100644
--- a/src/mol-model-formats/structure/pdb/to-cif.ts
+++ b/src/mol-model-formats/structure/pdb/to-cif.ts
@@ -162,8 +162,8 @@ export async function pdbToMmCif(pdb: PdbFile): Promise<CifFrame> {
     }
 
     const categories = {
-        entity: entityBuilder.getEntityTable(),
-        chem_comp: componentBuilder.getChemCompTable(),
+        entity: CifCategory.ofTable('entity', entityBuilder.getEntityTable()),
+        chem_comp: CifCategory.ofTable('chem_comp', componentBuilder.getChemCompTable()),
         atom_site: CifCategory.ofFields('atom_site', getAtomSite(atomSite)),
         atom_site_anisotrop: CifCategory.ofFields('atom_site_anisotrop', getAnisotropic(anisotropic))
     } as any;
diff --git a/src/mol-model-formats/structure/property/pair-restraints/predicted-contacts.ts b/src/mol-model-formats/structure/property/pair-restraints/predicted-contacts.ts
deleted file mode 100644
index d736eabdf2c073838585736ba735813d76143d75..0000000000000000000000000000000000000000
--- a/src/mol-model-formats/structure/property/pair-restraints/predicted-contacts.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-// TODO
-// ihm_predicted_contact_restraint: {
-//     id: int,
-//     entity_id_1: str,
-//     entity_id_2: str,
-//     asym_id_1: str,
-//     asym_id_2: str,
-//     comp_id_1: str,
-//     comp_id_2: str,
-//     seq_id_1: int,
-//     seq_id_2: int,
-//     atom_id_1: str,
-//     atom_id_2: str,
-//     distance_upper_limit: float,
-//     probability: float,
-//     restraint_type: Aliased<'lower bound' | 'upper bound' | 'lower and upper bound'>(str),
-//     model_granularity: Aliased<'by-residue' | 'by-feature' | 'by-atom'>(str),
-//     dataset_list_id: int,
-//     software_id: int,
-// },
diff --git a/src/mol-model-props/common/custom-element-property.ts b/src/mol-model-props/common/custom-element-property.ts
index 3dea414a939e75449f7438195624b5387c5b300f..4684a20e428f90b48a8ec7f6b404faf13f19e911 100644
--- a/src/mol-model-props/common/custom-element-property.ts
+++ b/src/mol-model-props/common/custom-element-property.ts
@@ -99,6 +99,7 @@ namespace CustomElementProperty {
 
         return {
             label: modelProperty.label,
+            category: 'Custom',
             factory: Coloring,
             getParams: () => ({}),
             defaultValues: {},
diff --git a/src/mol-model-props/computed/accessible-surface-area.ts b/src/mol-model-props/computed/accessible-surface-area.ts
index 292ab54c6aef5fb52eed0c9259c4bec15fc9df91..b3867ca743589cb6f1fd8d90be17510dce2e1715 100644
--- a/src/mol-model-props/computed/accessible-surface-area.ts
+++ b/src/mol-model-props/computed/accessible-surface-area.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 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>
@@ -7,9 +7,12 @@
 
 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 { Structure, CustomPropertyDescriptor, Unit } from '../../mol-model/structure';
 import { CustomStructureProperty } from '../common/custom-structure-property';
 import { CustomProperty } from '../common/custom-property';
+import { QuerySymbolRuntime } from '../../mol-script/runtime/query/compiler';
+import { CustomPropSymbol } from '../../mol-script/language/symbol';
+import Type from '../../mol-script/language/type';
 
 export const AccessibleSurfaceAreaParams = {
     ...ShrakeRupleyComputationParams
@@ -17,13 +20,33 @@ export const AccessibleSurfaceAreaParams = {
 export type AccessibleSurfaceAreaParams = typeof AccessibleSurfaceAreaParams
 export type AccessibleSurfaceAreaProps = PD.Values<AccessibleSurfaceAreaParams>
 
+export const AccessibleSurfaceAreaSymbols = {
+    isBuried: QuerySymbolRuntime.Dynamic(CustomPropSymbol('computed', 'accessible-surface-area.is-buried', Type.Bool),
+        ctx => {
+            if (!Unit.isAtomic(ctx.element.unit)) return false
+            const accessibleSurfaceArea = AccessibleSurfaceAreaProvider.get(ctx.element.structure).value
+            if (!accessibleSurfaceArea) return false
+            return AccessibleSurfaceArea.getFlag(ctx.element, accessibleSurfaceArea) === AccessibleSurfaceArea.Flag.Buried
+        }
+    ),
+    isAccessible: QuerySymbolRuntime.Dynamic(CustomPropSymbol('computed', 'accessible-surface-area.is-accessible', Type.Bool),
+        ctx => {
+            if (!Unit.isAtomic(ctx.element.unit)) return false
+            const accessibleSurfaceArea = AccessibleSurfaceAreaProvider.get(ctx.element.structure).value
+            if (!accessibleSurfaceArea) return false
+            return AccessibleSurfaceArea.getFlag(ctx.element, accessibleSurfaceArea) === AccessibleSurfaceArea.Flag.Accessible
+        }
+    ),
+}
+
 export type AccessibleSurfaceAreaValue = AccessibleSurfaceArea
 
 export const AccessibleSurfaceAreaProvider: CustomStructureProperty.Provider<AccessibleSurfaceAreaParams, AccessibleSurfaceAreaValue> = CustomStructureProperty.createProvider({
     label: 'Accessible Surface Area',
     descriptor: CustomPropertyDescriptor({
         name: 'molstar_accessible_surface_area',
-        // TODO `cifExport` and `symbol`
+        symbols: AccessibleSurfaceAreaSymbols,
+        // TODO `cifExport`
     }),
     type: 'root',
     defaultParams: AccessibleSurfaceAreaParams,
diff --git a/src/mol-model-props/computed/accessible-surface-area/shrake-rupley.ts b/src/mol-model-props/computed/accessible-surface-area/shrake-rupley.ts
index 6212d89f131d9826f87acb25af0a7b741c788223..b7b116061901f1357ffc613bcae58cb7070ac922 100644
--- a/src/mol-model-props/computed/accessible-surface-area/shrake-rupley.ts
+++ b/src/mol-model-props/computed/accessible-surface-area/shrake-rupley.ts
@@ -9,7 +9,7 @@ import { Task, RuntimeContext } from '../../../mol-task';
 // import { BitFlags } from '../../../mol-util';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition'
 import { Vec3 } from '../../../mol-math/linear-algebra';
-import { Structure } from '../../../mol-model/structure';
+import { Structure, StructureElement, StructureProperties } from '../../../mol-model/structure';
 import { assignRadiusForHeavyAtoms } from './shrake-rupley/radii';
 import { ShrakeRupleyContext, VdWLookup, MaxAsa, DefaultMaxAsa } from './shrake-rupley/common';
 import { computeArea } from './shrake-rupley/area';
@@ -86,19 +86,35 @@ namespace AccessibleSurfaceArea {
         return points;
     }
 
-    // export namespace SolventAccessibility {
-    //     export const is: (t: number, f: Flag) => boolean = BitFlags.has
-    //     export const create: (f: Flag) => number = BitFlags.create
-    //     export const enum Flag {
-    //         _ = 0x0,
-    //         BURIED = 0x1,
-    //         ACCESSIBLE = 0x2
-    //     }
-    // }
+    export const enum Flag {
+        NA = 0x0,
+        Buried = 0x1,
+        Accessible = 0x2
+    }
 
     /** Get relative area for a given component id */
     export function normalize(compId: string, asa: number) {
         const maxAsa = MaxAsa[compId] || DefaultMaxAsa;
         return asa / maxAsa
     }
+
+    export function getValue(location: StructureElement.Location, accessibleSurfaceArea: AccessibleSurfaceArea) {
+        const { getSerialIndex } = location.structure.root.serialMapping
+        const { area, serialResidueIndex } = accessibleSurfaceArea
+        const rSI = serialResidueIndex[getSerialIndex(location.unit, location.element)]
+        if (rSI === -1) return -1
+        return area[rSI]
+    }
+
+    export function getNormalizedValue(location: StructureElement.Location, accessibleSurfaceArea: AccessibleSurfaceArea) {
+        const value = getValue(location, accessibleSurfaceArea)
+        return value === -1 ? -1 : normalize(StructureProperties.residue.label_comp_id(location), value)
+    }
+
+    export function getFlag(location: StructureElement.Location, accessibleSurfaceArea: AccessibleSurfaceArea) {
+        const value = getNormalizedValue(location, accessibleSurfaceArea)
+        return value === -1 ? Flag.NA :
+            value < 0.16 ? Flag.Buried :
+                Flag.Accessible
+    }
 }
\ No newline at end of file
diff --git a/src/mol-model-props/computed/interactions/interactions.ts b/src/mol-model-props/computed/interactions/interactions.ts
index 881163733e3549460322b933d2297b734621be20..85411d94db757a29b5eaedb7a75b81f5eccb07f3 100644
--- a/src/mol-model-props/computed/interactions/interactions.ts
+++ b/src/mol-model-props/computed/interactions/interactions.ts
@@ -38,8 +38,9 @@ interface Interactions {
 }
 
 namespace Interactions {
+    type StructureInteractions = { readonly structure: Structure, readonly interactions: Interactions }
+
     export interface Element {
-        structure: Structure,
         unitA: Unit
         /** Index into features of unitA */
         indexA: Features.FeatureIndex
@@ -47,11 +48,12 @@ namespace Interactions {
         /** Index into features of unitB */
         indexB: Features.FeatureIndex
     }
-    export interface Location extends DataLocation<Interactions, Element> {}
+
+    export interface Location extends DataLocation<StructureInteractions, Element> {}
 
     export function Location(interactions: Interactions, structure: Structure, unitA?: Unit, indexA?: Features.FeatureIndex, unitB?: Unit, indexB?: Features.FeatureIndex): Location {
-        return DataLocation('interactions', interactions, 
-            { structure: structure as any, unitA: unitA as any, indexA: indexA as any, unitB: unitB as any, indexB: indexB as any });
+        return DataLocation('interactions', { structure, interactions },
+            { unitA: unitA as any, indexA: indexA as any, unitB: unitB as any, indexB: indexB as any });
     }
 
     export function isLocation(x: any): x is Location {
@@ -60,7 +62,8 @@ namespace Interactions {
 
     export function areLocationsEqual(locA: Location, locB: Location) {
         return (
-            locA.data === locB.data &&
+            locA.data.structure === locB.data.structure &&
+            locA.data.interactions === locB.data.interactions &&
             locA.element.indexA === locB.element.indexA &&
             locA.element.indexB === locB.element.indexB &&
             locA.element.unitA === locB.element.unitA &&
@@ -82,10 +85,9 @@ namespace Interactions {
     }
 
     export function locationLabel(location: Location): string {
-        return _label(location.data, location.element)
+        return _label(location.data.interactions, location.element)
     }
 
-    type StructureInteractions = { readonly structure: Structure, readonly interactions: Interactions }
     export interface Loci extends DataLoci<StructureInteractions, Element> { }
 
     export function Loci(structure: Structure, interactions: Interactions, elements: ReadonlyArray<Element>): Loci {
diff --git a/src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts b/src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts
index a13a95720688fd97ff2f042fa9ad144fb688971e..1895d5e5536bcc212138f0ec3fda1c01c2ae6d68 100644
--- a/src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts
+++ b/src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts
@@ -101,8 +101,8 @@ function getInteractionLoci(pickingId: PickingId, structure: Structure, id: numb
         const interactions = InteractionsProvider.get(structure).value!
         const c = interactions.contacts.edges[groupId]
         return Interactions.Loci(structure, interactions, [
-            { structure, unitA: c.unitA, indexA: c.indexA, unitB: c.unitB, indexB: c.indexB },
-            { structure, unitA: c.unitB, indexA: c.indexB, unitB: c.unitA, indexB: c.indexA },
+            { unitA: c.unitA, indexA: c.indexA, unitB: c.unitB, indexB: c.indexB },
+            { unitA: c.unitB, indexA: c.indexB, unitB: c.unitA, indexB: c.indexA },
         ])
     }
     return EmptyLoci
diff --git a/src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts b/src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts
index 66a03f6ac7ee24d281c2251051e40bbf4e9a22ba..3e0b864a7bfb1718587e070f2b68718fbda39d95 100644
--- a/src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts
+++ b/src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts
@@ -97,8 +97,8 @@ function getInteractionLoci(pickingId: PickingId, structureGroup: StructureGroup
         const interactions = InteractionsProvider.get(structure).value!
         const { a, b } = interactions.unitsContacts.get(unit.id)
         return Interactions.Loci(structure, interactions, [
-            { structure, unitA: unit, indexA: a[groupId], unitB: unit, indexB: b[groupId] },
-            { structure, unitA: unit, indexA: b[groupId], unitB: unit, indexB: a[groupId] },
+            { unitA: unit, indexA: a[groupId], unitB: unit, indexB: b[groupId] },
+            { unitA: unit, indexA: b[groupId], unitB: unit, indexB: a[groupId] },
         ])
     }
     return EmptyLoci
diff --git a/src/mol-model-props/computed/secondary-structure.ts b/src/mol-model-props/computed/secondary-structure.ts
index d80c58b383b9a8725daed90eb9632203986e2f17..5a373f6d0334ee5065675fa655cb6809c7e1ecd6 100644
--- a/src/mol-model-props/computed/secondary-structure.ts
+++ b/src/mol-model-props/computed/secondary-structure.ts
@@ -1,10 +1,10 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { CustomPropertyDescriptor, Structure } from '../../mol-model/structure';
+import { Structure } from '../../mol-model/structure';
 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';
@@ -13,9 +13,10 @@ import { CustomStructureProperty } from '../common/custom-structure-property';
 import { CustomProperty } from '../common/custom-property';
 import { ModelSecondaryStructure } from '../../mol-model-formats/structure/property/secondary-structure';
 import { MmcifFormat } from '../../mol-model-formats/structure/mmcif';
+import { CustomPropertyDescriptor } from '../../mol-model/structure/common/custom-property';
 
 function getSecondaryStructureParams(data?: Structure) {
-    let defaultType = 'mmcif' as 'mmcif' | 'dssp'
+    let defaultType = 'model' as 'model' | 'dssp'
     if (data) {
         defaultType = 'dssp'
         for (let i = 0, il = data.models.length; i < il; ++i) {
@@ -27,7 +28,7 @@ function getSecondaryStructureParams(data?: Structure) {
                 ) {
                     // if there is any secondary structure definition given or if there is
                     // an archival model, don't calculate dssp by default
-                    defaultType = 'mmcif'
+                    defaultType = 'model'
                     break
                 }
             }
@@ -35,9 +36,9 @@ function getSecondaryStructureParams(data?: Structure) {
     }
     return {
         type: PD.MappedStatic(defaultType, {
-            'mmcif': PD.EmptyGroup({ label: 'mmCIF' }),
+            'model': PD.EmptyGroup({ label: 'Model' }),
             'dssp': PD.Group(DSSPComputationParams, { label: 'DSSP', isFlat: true })
-        }, { options: [['mmcif', 'mmCIF'], ['dssp', 'DSSP']] })
+        }, { options: [['model', 'Model'], ['dssp', 'DSSP']] })
     }
 }
 
@@ -45,6 +46,7 @@ export const SecondaryStructureParams = getSecondaryStructureParams()
 export type SecondaryStructureParams = typeof SecondaryStructureParams
 export type SecondaryStructureProps = PD.Values<SecondaryStructureParams>
 
+/** Maps `unit.id` to `SecondaryStructure` */
 export type SecondaryStructureValue = Map<number, SecondaryStructure>
 
 export const SecondaryStructureProvider: CustomStructureProperty.Provider<SecondaryStructureParams, SecondaryStructureValue> = CustomStructureProperty.createProvider({
@@ -61,7 +63,7 @@ export const SecondaryStructureProvider: CustomStructureProperty.Provider<Second
         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)
+            case 'model': return await computeModel(data)
         }
     }
 })
@@ -80,7 +82,7 @@ async function computeDssp(structure: Structure, props: DSSPComputationProps): P
     return map
 }
 
-async function computeMmcif(structure: Structure): Promise<SecondaryStructureValue> {
+async function computeModel(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]
diff --git a/src/mol-model-props/computed/themes/accessible-surface-area.ts b/src/mol-model-props/computed/themes/accessible-surface-area.ts
index fe2a60134c831deb06d546d4bec2c49f7ec8252c..5de7df9837f50baec785434be678d0ebf43c8cec 100644
--- a/src/mol-model-props/computed/themes/accessible-surface-area.ts
+++ b/src/mol-model-props/computed/themes/accessible-surface-area.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 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>
@@ -10,7 +10,7 @@ import { ParamDefinition as PD } from '../../../mol-util/param-definition'
 import { Color, ColorScale } from '../../../mol-util/color'
 import { ThemeDataContext } from '../../../mol-theme/theme'
 import { ColorTheme, LocationColor } from '../../../mol-theme/color'
-import { StructureProperties, StructureElement, Unit } from '../../../mol-model/structure'
+import { StructureElement, Unit } from '../../../mol-model/structure'
 import { AccessibleSurfaceAreaProvider } from '../accessible-surface-area'
 import { AccessibleSurfaceArea } from '../accessible-surface-area/shrake-rupley'
 import { CustomProperty } from '../../common/custom-property'
@@ -36,18 +36,16 @@ export function AccessibleSurfaceAreaColorTheme(ctx: ThemeDataContext, props: PD
         domain: [0.0, 1.0]
     })
 
-    const { label_comp_id } = StructureProperties.residue
     const accessibleSurfaceArea = ctx.structure && AccessibleSurfaceAreaProvider.get(ctx.structure)
     const contextHash = accessibleSurfaceArea?.version
 
     if (accessibleSurfaceArea?.value && ctx.structure) {
-        const { getSerialIndex } = ctx.structure.root.serialMapping
-        const { area, serialResidueIndex } = accessibleSurfaceArea.value
+        const asa = accessibleSurfaceArea.value
 
         color = (location: Location): Color => {
             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]))
+                const value = AccessibleSurfaceArea.getNormalizedValue(location, asa)
+                return value === -1 ? DefaultColor : scale.color(value)
             }
             return DefaultColor
         }
@@ -68,6 +66,7 @@ export function AccessibleSurfaceAreaColorTheme(ctx: ThemeDataContext, props: PD
 
 export const AccessibleSurfaceAreaColorThemeProvider: ColorTheme.Provider<AccessibleSurfaceAreaColorThemeParams> = {
     label: 'Accessible Surface Area',
+    category: ColorTheme.Category.Residue,
     factory: AccessibleSurfaceAreaColorTheme,
     getParams: getAccessibleSurfaceAreaColorThemeParams,
     defaultValues: PD.getDefaultValues(AccessibleSurfaceAreaColorThemeParams),
diff --git a/src/mol-model-props/computed/themes/interaction-type.ts b/src/mol-model-props/computed/themes/interaction-type.ts
index 32e0a9174c3494f48f07bf930b5cdb824d6d4329..70c88fd04979d9616f22bfd086f191b9a65c56a4 100644
--- a/src/mol-model-props/computed/themes/interaction-type.ts
+++ b/src/mol-model-props/computed/themes/interaction-type.ts
@@ -78,7 +78,7 @@ export function InteractionTypeColorTheme(ctx: ThemeDataContext, props: PD.Value
     if (interactions && interactions.value) {
         color = (location: Location) => {
             if (Interactions.isLocation(location)) {
-                const { unitsContacts, contacts } = location.data
+                const { unitsContacts, contacts } = location.data.interactions
                 const { unitA, unitB, indexA, indexB } = location.element
                 if (unitA === unitB) {
                     const links = unitsContacts.get(unitA.id)
@@ -108,6 +108,7 @@ export function InteractionTypeColorTheme(ctx: ThemeDataContext, props: PD.Value
 
 export const InteractionTypeColorThemeProvider: ColorTheme.Provider<InteractionTypeColorThemeParams> = {
     label: 'Interaction Type',
+    category: ColorTheme.Category.Misc,
     factory: InteractionTypeColorTheme,
     getParams: getInteractionTypeColorThemeParams,
     defaultValues: PD.getDefaultValues(InteractionTypeColorThemeParams),
diff --git a/src/mol-theme/color/cross-link.ts b/src/mol-model-props/integrative/cross-link-restraint/color.ts
similarity index 51%
rename from src/mol-theme/color/cross-link.ts
rename to src/mol-model-props/integrative/cross-link-restraint/color.ts
index d744e44b4cc9f22126a9c018173b25d1a09f8fbd..0ff69cd16f2a016ca9907c76aa0ab662570255e3 100644
--- a/src/mol-theme/color/cross-link.ts
+++ b/src/mol-model-props/integrative/cross-link-restraint/color.ts
@@ -1,23 +1,23 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Bond } from '../../mol-model/structure';
-import { Color, ColorScale } from '../../mol-util/color';
-import { Location } from '../../mol-model/location';
-import { ColorTheme, LocationColor } from '../color';
-import { Vec3 } from '../../mol-math/linear-algebra';
-import { ParamDefinition as PD } from '../../mol-util/param-definition'
-import { ThemeDataContext } from '../../mol-theme/theme';
-import { ColorListName, ColorListOptionsScale } from '../../mol-util/color/lists';
+import { Color, ColorScale } from '../../../mol-util/color';
+import { Location } from '../../../mol-model/location';
+import { ParamDefinition as PD } from '../../../mol-util/param-definition'
+import { ThemeDataContext } from '../../../mol-theme/theme';
+import { ColorListName, ColorListOptionsScale } from '../../../mol-util/color/lists';
+import { ColorTheme, LocationColor } from '../../../mol-theme/color';
+import { CustomProperty } from '../../common/custom-property';
+import { CrossLinkRestraintProvider, CrossLinkRestraint } from './property';
 
 const DefaultColor = Color(0xCCCCCC)
-const Description = 'Colors cross-links by the deviation of the observed distance versus the modeled distance (e.g. `ihm_cross_link_restraint.distance_threshold`).'
+const Description = 'Colors cross-links by the deviation of the observed distance versus the modeled distance (e.g. modeled / `ihm_cross_link_restraint.distance_threshold`).'
 
 export const CrossLinkColorThemeParams = {
-    domain: PD.Interval([-10, 10]),
+    domain: PD.Interval([0.5, 1.5], { step: 0.01 }),
     list: PD.ColorList<ColorListName>('red-grey', ColorListOptionsScale),
 }
 export type CrossLinkColorThemeParams = typeof CrossLinkColorThemeParams
@@ -25,19 +25,13 @@ export function getCrossLinkColorThemeParams(ctx: ThemeDataContext) {
     return CrossLinkColorThemeParams // TODO return copy
 }
 
-const distVecA = Vec3.zero(), distVecB = Vec3.zero()
-function linkDistance(link: Bond.Location) {
-    link.aUnit.conformation.position(link.aUnit.elements[link.aIndex], distVecA)
-    link.bUnit.conformation.position(link.bUnit.elements[link.bIndex], distVecB)
-    return Vec3.distance(distVecA, distVecB)
-}
-
 export function CrossLinkColorTheme(ctx: ThemeDataContext, props: PD.Values<CrossLinkColorThemeParams>): ColorTheme<CrossLinkColorThemeParams> {
     let color: LocationColor
     let scale: ColorScale | undefined = undefined
 
-    if (ctx.structure) {
-        const crosslinks = ctx.structure.crossLinkRestraints
+    const crossLinkRestraints = ctx.structure && CrossLinkRestraintProvider.get(ctx.structure).value
+
+    if (crossLinkRestraints) {
         scale = ColorScale.create({
             domain: props.domain,
             listOrName: props.list
@@ -45,10 +39,10 @@ export function CrossLinkColorTheme(ctx: ThemeDataContext, props: PD.Values<Cros
         const scaleColor = scale.color
 
         color = (location: Location): Color => {
-            if (Bond.isLocation(location)) {
-                const pairs = crosslinks.getPairs(location.aIndex, location.aUnit, location.bIndex, location.bUnit)
-                if (pairs) {
-                    return scaleColor(linkDistance(location) - pairs[0].distanceThreshold)
+            if (CrossLinkRestraint.isLocation(location)) {
+                const pair = crossLinkRestraints.pairs[location.element]
+                if (pair) {
+                    return scaleColor(CrossLinkRestraint.distance(pair) / pair.distanceThreshold)
                 }
             }
             return DefaultColor
@@ -69,8 +63,12 @@ export function CrossLinkColorTheme(ctx: ThemeDataContext, props: PD.Values<Cros
 
 export const CrossLinkColorThemeProvider: ColorTheme.Provider<CrossLinkColorThemeParams> = {
     label: 'Cross Link',
+    category: ColorTheme.Category.Misc,
     factory: CrossLinkColorTheme,
     getParams: getCrossLinkColorThemeParams,
     defaultValues: PD.getDefaultValues(CrossLinkColorThemeParams),
-    isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.crossLinkRestraints.count > 0
+    isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && CrossLinkRestraint.isApplicable(ctx.structure),
+    ensureCustomProperties: (ctx: CustomProperty.Context, data: ThemeDataContext) => {
+        return data.structure ? CrossLinkRestraintProvider.attach(ctx, data.structure) : Promise.resolve()
+    }
 }
\ No newline at end of file
diff --git a/src/mol-model-formats/structure/property/pair-restraints/cross-links.ts b/src/mol-model-props/integrative/cross-link-restraint/format.ts
similarity index 89%
rename from src/mol-model-formats/structure/property/pair-restraints/cross-links.ts
rename to src/mol-model-props/integrative/cross-link-restraint/format.ts
index 484645a3a921c46bc2aa3f63aa26766043b3569d..a4e9c0c738a9aaaace9628f3fcb4497e54892b43 100644
--- a/src/mol-model-formats/structure/property/pair-restraints/cross-links.ts
+++ b/src/mol-model-props/integrative/cross-link-restraint/format.ts
@@ -4,12 +4,12 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Model } from '../../../../mol-model/structure/model/model'
-import { Table } from '../../../../mol-data/db'
-import { mmCIF_Schema } from '../../../../mol-io/reader/cif/schema/mmcif';
-import { Unit, CustomPropertyDescriptor } from '../../../../mol-model/structure';
-import { ElementIndex } from '../../../../mol-model/structure/model/indexing';
-import { FormatPropertyProvider } from '../../common/property';
+import { Model } from '../../../mol-model/structure/model/model'
+import { Table } from '../../../mol-data/db'
+import { mmCIF_Schema } from '../../../mol-io/reader/cif/schema/mmcif';
+import { Unit, CustomPropertyDescriptor } from '../../../mol-model/structure';
+import { ElementIndex } from '../../../mol-model/structure/model/indexing';
+import { FormatPropertyProvider } from '../../../mol-model-formats/structure/common/property';
 
 export { ModelCrossLinkRestraint }
 
diff --git a/src/mol-model-props/integrative/cross-link-restraint/property.ts b/src/mol-model-props/integrative/cross-link-restraint/property.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5deb93896a949af60ea84b5774a24e7ccb183bb4
--- /dev/null
+++ b/src/mol-model-props/integrative/cross-link-restraint/property.ts
@@ -0,0 +1,221 @@
+/**
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ModelCrossLinkRestraint } from './format';
+import { Unit, StructureElement, Structure, CustomPropertyDescriptor, Bond} from '../../../mol-model/structure';
+import { PairRestraints, PairRestraint } from '../pair-restraints';
+import { CustomStructureProperty } from '../../common/custom-structure-property';
+import { CustomProperty } from '../../common/custom-property';
+import { DataLocation } from '../../../mol-model/location';
+import { DataLoci } from '../../../mol-model/loci';
+import { Sphere3D } from '../../../mol-math/geometry';
+import { CentroidHelper } from '../../../mol-math/geometry/centroid-helper';
+import { bondLabel } from '../../../mol-theme/label';
+import { Vec3 } from '../../../mol-math/linear-algebra';
+
+export type CrossLinkRestraintValue = PairRestraints<CrossLinkRestraint>
+
+export const CrossLinkRestraintProvider: CustomStructureProperty.Provider<{}, CrossLinkRestraintValue> = CustomStructureProperty.createProvider({
+    label: 'Cross Link Restraint',
+    descriptor: CustomPropertyDescriptor({
+        name: 'integrative-cross-link-restraint',
+        // TODO `cifExport` and `symbol`
+    }),
+    type: 'local',
+    defaultParams: {},
+    getParams: (data: Structure) => ({}),
+    isApplicable: (data: Structure) => data.models.some(m => !!ModelCrossLinkRestraint.Provider.get(m)),
+    obtain: async (ctx: CustomProperty.Context, data: Structure, props: Partial<{}>) => {
+        return extractCrossLinkRestraints(data)
+    }
+})
+
+export { CrossLinkRestraint }
+
+interface CrossLinkRestraint extends PairRestraint {
+    readonly restraintType: 'harmonic' | 'upper bound' | 'lower bound'
+    readonly distanceThreshold: number
+    readonly psi: number
+    readonly sigma1: number
+    readonly sigma2: number
+}
+
+namespace CrossLinkRestraint {
+    export enum Tag {
+        CrossLinkRestraint = 'cross-link-restraint'
+    }
+
+    export function isApplicable(structure: Structure) {
+        return structure.models.some(m => !!ModelCrossLinkRestraint.Provider.get(m))
+    }
+
+    const distVecA = Vec3(), distVecB = Vec3()
+    export function distance(pair: CrossLinkRestraint) {
+        pair.unitA.conformation.position(pair.unitA.elements[pair.indexA], distVecA)
+        pair.unitB.conformation.position(pair.unitB.elements[pair.indexB], distVecB)
+        return Vec3.distance(distVecA, distVecB)
+    }
+
+    type StructureCrossLinkRestraints = { readonly structure: Structure, readonly crossLinkRestraints: CrossLinkRestraintValue }
+
+    export type Element = number
+    export interface Location extends DataLocation<StructureCrossLinkRestraints, Element> {}
+
+    export function Location(crossLinkRestraints: CrossLinkRestraintValue, structure: Structure, index?: number): Location {
+        return DataLocation('cross-link-restraints', { structure, crossLinkRestraints }, index as any);
+    }
+
+    export function isLocation(x: any): x is Location {
+        return !!x && x.kind === 'data-location' && x.tag === 'cross-link-restraints';
+    }
+
+    export function areLocationsEqual(locA: Location, locB: Location) {
+        return (
+            locA.data.structure === locB.data.structure &&
+            locA.data.crossLinkRestraints === locB.data.crossLinkRestraints &&
+            locA.element === locB.element
+        )
+    }
+
+    function _label(crossLinkRestraints: CrossLinkRestraintValue, element: Element): string {
+        const p = crossLinkRestraints.pairs[element]
+        return `Cross Link Restraint | Type: ${p.restraintType} | Threshold: ${p.distanceThreshold} \u212B | Psi: ${p.psi} | Sigma 1: ${p.sigma1} | Sigma 2: ${p.sigma2} | Distance: ${distance(p).toFixed(2)} \u212B`
+    }
+
+    export function locationLabel(location: Location): string {
+        return _label(location.data.crossLinkRestraints, location.element)
+    }
+
+    export interface Loci extends DataLoci<StructureCrossLinkRestraints, Element> { }
+
+    export function Loci(structure: Structure, crossLinkRestraints: CrossLinkRestraintValue, elements: ReadonlyArray<Element>): Loci {
+        return DataLoci('cross-link-restraints', { structure, crossLinkRestraints }, elements,
+            (boundingSphere) => getBoundingSphere(crossLinkRestraints, elements, boundingSphere),
+            () => getLabel(structure, crossLinkRestraints, elements));
+    }
+
+    export function isLoci(x: any): x is Loci {
+        return !!x && x.kind === 'data-loci' && x.tag === 'interactions';
+    }
+
+    export function getBoundingSphere(crossLinkRestraints: CrossLinkRestraintValue, elements: ReadonlyArray<Element>, boundingSphere: Sphere3D) {
+        return CentroidHelper.fromPairProvider(elements.length, (i, pA, pB) => {
+            const p = crossLinkRestraints.pairs[elements[i]]
+            p.unitA.conformation.position(p.unitA.elements[p.indexA], pA)
+            p.unitB.conformation.position(p.unitB.elements[p.indexB], pB)
+        }, boundingSphere)
+    }
+
+    export function getLabel(structure: Structure, crossLinkRestraints: CrossLinkRestraintValue, elements: ReadonlyArray<Element>) {
+        const element = elements[0]
+        if (element === undefined) return ''
+        const p = crossLinkRestraints.pairs[element]
+        return [
+            _label(crossLinkRestraints, element),
+            bondLabel(Bond.Location(structure, p.unitA, p.indexA, structure, p.unitB, p.indexB))
+        ].join('</br>')
+    }
+}
+
+//
+
+function _addRestraints(map: Map<number, number>, unit: Unit, restraints: ModelCrossLinkRestraint) {
+    const { elements } = unit;
+    const elementCount = elements.length;
+    const kind = unit.kind
+
+    for (let i = 0; i < elementCount; i++) {
+        const e = elements[i];
+        restraints.getIndicesByElement(e, kind).forEach(ri => map.set(ri, i))
+    }
+}
+
+function extractInter(pairs: CrossLinkRestraint[], unitA: Unit, unitB: Unit) {
+    if (unitA.model !== unitB.model) return
+    if (unitA.model.sourceData.kind !== 'mmCIF') return
+
+    const restraints = ModelCrossLinkRestraint.Provider.get(unitA.model)
+    if (!restraints) return
+
+    const rA = new Map<number, StructureElement.UnitIndex>();
+    const rB = new Map<number, StructureElement.UnitIndex>();
+    _addRestraints(rA, unitA, restraints)
+    _addRestraints(rB, unitB, restraints)
+
+    rA.forEach((indexA, ri) => {
+        const indexB = rB.get(ri)
+        if (indexB !== undefined) {
+            pairs.push(
+                createCrossLinkRestraint(unitA, indexA, unitB, indexB, restraints, ri),
+                createCrossLinkRestraint(unitB, indexB, unitA, indexA, restraints, ri)
+            )
+        }
+    })
+}
+
+function extractIntra(pairs: CrossLinkRestraint[], unit: Unit) {
+    if (unit.model.sourceData.kind !== 'mmCIF') return
+
+    const restraints = ModelCrossLinkRestraint.Provider.get(unit.model)
+    if (!restraints) return
+
+    const { elements } = unit;
+    const elementCount = elements.length;
+    const kind = unit.kind
+
+    const r = new Map<number, StructureElement.UnitIndex[]>();
+
+    for (let i = 0; i < elementCount; i++) {
+        const e = elements[i];
+        restraints.getIndicesByElement(e, kind).forEach(ri => {
+            const il = r.get(ri)
+            if (il) il.push(i as StructureElement.UnitIndex)
+            else r.set(ri, [i as StructureElement.UnitIndex])
+        })
+    }
+
+    r.forEach((il, ri) => {
+        if (il.length < 2) return
+        const [ indexA, indexB ] = il
+        pairs.push(
+            createCrossLinkRestraint(unit, indexA, unit, indexB, restraints, ri),
+            createCrossLinkRestraint(unit, indexB, unit, indexA, restraints, ri)
+        )
+    })
+}
+
+function createCrossLinkRestraint(unitA: Unit, indexA: StructureElement.UnitIndex, unitB: Unit, indexB: StructureElement.UnitIndex, restraints: ModelCrossLinkRestraint, row: number): CrossLinkRestraint {
+    return {
+        unitA, indexA, unitB, indexB,
+
+        restraintType: restraints.data.restraint_type.value(row),
+        distanceThreshold: restraints.data.distance_threshold.value(row),
+        psi: restraints.data.psi.value(row),
+        sigma1: restraints.data.sigma_1.value(row),
+        sigma2: restraints.data.sigma_2.value(row),
+    }
+}
+
+function extractCrossLinkRestraints(structure: Structure): PairRestraints<CrossLinkRestraint> {
+    const pairs: CrossLinkRestraint[] = []
+    if (!structure.models.some(m => ModelCrossLinkRestraint.Provider.get(m))) {
+        return new PairRestraints(pairs)
+    }
+
+    const n = structure.units.length
+    for (let i = 0; i < n; ++i) {
+        const unitA = structure.units[i]
+        extractIntra(pairs, unitA)
+        for (let j = i + 1; j < n; ++j) {
+            const unitB = structure.units[j]
+            if (unitA.model === unitB.model) {
+                extractInter(pairs, unitA, unitB)
+            }
+        }
+    }
+
+    return new PairRestraints(pairs)
+}
\ No newline at end of file
diff --git a/src/mol-model-props/integrative/cross-link-restraint/representation.ts b/src/mol-model-props/integrative/cross-link-restraint/representation.ts
new file mode 100644
index 0000000000000000000000000000000000000000..31718c637cfc91321088ad32d386856011d639d1
--- /dev/null
+++ b/src/mol-model-props/integrative/cross-link-restraint/representation.ts
@@ -0,0 +1,149 @@
+/**
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Representation, RepresentationContext, RepresentationParamsGetter } from '../../../mol-repr/representation';
+import { ThemeRegistryContext } from '../../../mol-theme/theme';
+import { Theme } from '../../../mol-theme/theme';
+import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
+import { Vec3 } from '../../../mol-math/linear-algebra';
+import { LocationIterator } from '../../../mol-geo/util/location-iterator';
+import { PickingId } from '../../../mol-geo/geometry/picking';
+import { EmptyLoci, Loci } from '../../../mol-model/loci';
+import { Interval } from '../../../mol-data/int';
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { Structure, StructureElement } from '../../../mol-model/structure';
+import { VisualContext } from '../../../mol-repr/visual';
+import { createLinkCylinderMesh, LinkCylinderParams } from '../../../mol-repr/structure/visual/util/link';
+import { ComplexMeshParams, ComplexVisual, ComplexMeshVisual } from '../../../mol-repr/structure/complex-visual';
+import { VisualUpdateState } from '../../../mol-repr/util';
+import { ComplexRepresentation, StructureRepresentation, StructureRepresentationStateBuilder, StructureRepresentationProvider } from '../../../mol-repr/structure/representation';
+import { UnitKind, UnitKindOptions } from '../../../mol-repr/structure/visual/util/common';
+import { CustomProperty } from '../../common/custom-property';
+import { CrossLinkRestraintProvider, CrossLinkRestraint } from './property';
+
+function createCrossLinkRestraintCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<CrossLinkRestraintCylinderParams>, mesh?: Mesh) {
+
+    const crossLinks = CrossLinkRestraintProvider.get(structure).value!
+    if (!crossLinks.count) return Mesh.createEmpty(mesh)
+    const { sizeFactor } = props
+
+    const location = StructureElement.Location.create(structure)
+
+    const builderProps = {
+        linkCount: crossLinks.count,
+        position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
+            const b = crossLinks.pairs[edgeIndex]
+            const uA = b.unitA, uB = b.unitB
+            uA.conformation.position(uA.elements[b.indexA], posA)
+            uB.conformation.position(uB.elements[b.indexB], posB)
+        },
+        radius: (edgeIndex: number) => {
+            const b = crossLinks.pairs[edgeIndex]
+            location.unit = b.unitA
+            location.element = b.unitA.elements[b.indexA]
+            return theme.size.size(location) * sizeFactor
+        },
+    }
+
+    return createLinkCylinderMesh(ctx, builderProps, props, mesh)
+}
+
+export const CrossLinkRestraintCylinderParams = {
+    ...ComplexMeshParams,
+    ...LinkCylinderParams,
+    sizeFactor: PD.Numeric(0.5, { min: 0, max: 10, step: 0.1 }),
+}
+export type CrossLinkRestraintCylinderParams = typeof CrossLinkRestraintCylinderParams
+
+export function CrossLinkRestraintVisual(materialId: number): ComplexVisual<CrossLinkRestraintCylinderParams> {
+    return ComplexMeshVisual<CrossLinkRestraintCylinderParams>({
+        defaultProps: PD.getDefaultValues(CrossLinkRestraintCylinderParams),
+        createGeometry: createCrossLinkRestraintCylinderMesh,
+        createLocationIterator: createCrossLinkRestraintIterator,
+        getLoci: getLinkLoci,
+        eachLocation: eachCrossLink,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<CrossLinkRestraintCylinderParams>, currentProps: PD.Values<CrossLinkRestraintCylinderParams>) => {
+            state.createGeometry = (
+                newProps.sizeFactor !== currentProps.sizeFactor ||
+                newProps.radialSegments !== currentProps.radialSegments ||
+                newProps.linkCap !== currentProps.linkCap
+            )
+        }
+    }, materialId)
+}
+
+function createCrossLinkRestraintIterator(structure: Structure): LocationIterator {
+    const crossLinkRestraints = CrossLinkRestraintProvider.get(structure).value!
+    const { pairs } = crossLinkRestraints
+    const groupCount = pairs.length
+    const instanceCount = 1
+    const location = CrossLinkRestraint.Location(crossLinkRestraints, structure)
+    const getLocation = (groupIndex: number) => {
+        location.element = groupIndex
+        return location
+    }
+    return LocationIterator(groupCount, instanceCount, getLocation, true)
+}
+
+function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) {
+    const { objectId, groupId } = pickingId
+    if (id === objectId) {
+        const crossLinkRestraints = CrossLinkRestraintProvider.get(structure).value!
+        const pair = crossLinkRestraints.pairs[groupId]
+        if (pair) {
+            return CrossLinkRestraint.Loci(structure, crossLinkRestraints, [groupId])
+        }
+    }
+    return EmptyLoci
+}
+
+function eachCrossLink(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
+    let changed = false
+    if (CrossLinkRestraint.isLoci(loci)) {
+        if (!Structure.areEquivalent(loci.data.structure, structure)) return false
+        const crossLinkRestraints = CrossLinkRestraintProvider.get(structure).value!
+        if (loci.data.crossLinkRestraints !== crossLinkRestraints) return false
+
+        for (const e of loci.elements) {
+            if (apply(Interval.ofSingleton(e))) changed = true
+        }
+    }
+    return changed
+}
+
+//
+
+const CrossLinkRestraintVisuals = {
+    'cross-link-restraint': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, CrossLinkRestraintCylinderParams>) => ComplexRepresentation('Cross-link restraint', ctx, getParams, CrossLinkRestraintVisual),
+}
+
+export const CrossLinkRestraintParams = {
+    ...CrossLinkRestraintCylinderParams,
+    unitKinds: PD.MultiSelect<UnitKind>(['atomic', 'spheres'], UnitKindOptions),
+}
+export type CrossLinkRestraintParams = typeof CrossLinkRestraintParams
+export function getCrossLinkRestraintParams(ctx: ThemeRegistryContext, structure: Structure) {
+    return PD.clone(CrossLinkRestraintParams)
+}
+
+export type CrossLinkRestraintRepresentation = StructureRepresentation<CrossLinkRestraintParams>
+export function CrossLinkRestraintRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, CrossLinkRestraintParams>): CrossLinkRestraintRepresentation {
+    return Representation.createMulti('CrossLinkRestraint', ctx, getParams, StructureRepresentationStateBuilder, CrossLinkRestraintVisuals as unknown as Representation.Def<Structure, CrossLinkRestraintParams>)
+}
+
+export const CrossLinkRestraintRepresentationProvider: StructureRepresentationProvider<CrossLinkRestraintParams> = {
+    label: 'Cross Link Restraint',
+    description: 'Displays cross-link restraints.',
+    factory: CrossLinkRestraintRepresentation,
+    getParams: getCrossLinkRestraintParams,
+    defaultValues: PD.getDefaultValues(CrossLinkRestraintParams),
+    defaultColorTheme: { name: CrossLinkRestraint.Tag.CrossLinkRestraint },
+    defaultSizeTheme: { name: 'uniform' },
+    isApplicable: (structure: Structure) => CrossLinkRestraint.isApplicable(structure),
+    ensureCustomProperties: (ctx: CustomProperty.Context, structure: Structure) => {
+        return CrossLinkRestraintProvider.attach(ctx, structure)
+    }
+}
\ No newline at end of file
diff --git a/src/mol-model-props/integrative/pair-restraints.ts b/src/mol-model-props/integrative/pair-restraints.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7a74a4412957f165d44893263ec4a01082894c01
--- /dev/null
+++ b/src/mol-model-props/integrative/pair-restraints.ts
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { StructureElement, Unit } from '../../mol-model/structure';
+
+const emptyArray: number[] = []
+
+export interface PairRestraint {
+    readonly unitA: Unit,
+    readonly unitB: Unit,
+    readonly indexA: StructureElement.UnitIndex,
+    readonly indexB: StructureElement.UnitIndex,
+}
+
+function getPairKey(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit) {
+    return `${indexA}|${unitA.id}|${indexB}|${unitB.id}`
+}
+
+export class PairRestraints<T extends PairRestraint> {
+    readonly count: number
+    private readonly pairKeyIndices: Map<string, number[]>
+
+    /** Indices into this.pairs */
+    getPairIndices(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit): ReadonlyArray<number> {
+        const key = getPairKey(indexA, unitA, indexB, unitB)
+        return this.pairKeyIndices.get(key) || emptyArray
+    }
+
+    getPairs(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit): T[] {
+        const indices = this.getPairIndices(indexA, unitA, indexB, unitB)
+        return indices.map(idx => this.pairs[idx])
+    }
+
+    constructor(public pairs: ReadonlyArray<T>) {
+        const pairKeyIndices = new Map<string, number[]>()
+        this.pairs.forEach((p, i) => {
+            const key = getPairKey(p.indexA, p.unitA, p.indexB, p.unitB)
+            const indices = pairKeyIndices.get(key)
+            if (indices) indices.push(i)
+            else pairKeyIndices.set(key, [i])
+        })
+
+        this.count = pairs.length
+        this.pairKeyIndices = pairKeyIndices
+    }
+}
\ No newline at end of file
diff --git a/src/mol-model-props/pdbe/structure-quality-report.ts b/src/mol-model-props/pdbe/structure-quality-report.ts
index e153de014ba3749c37f0700edb55b9f0a01698b3..1950f925299c9d2e8f684e5831dbefe1f36e870c 100644
--- a/src/mol-model-props/pdbe/structure-quality-report.ts
+++ b/src/mol-model-props/pdbe/structure-quality-report.ts
@@ -119,7 +119,7 @@ export type StructureQualityReportParams = typeof StructureQualityReportParams
 export type StructureQualityReportProps = PD.Values<StructureQualityReportParams>
 
 export const StructureQualityReportProvider: CustomModelProperty.Provider<StructureQualityReportParams, StructureQualityReport> = CustomModelProperty.createProvider({
-    label: 'PDBe Structure Quality Report',
+    label: 'Structure Quality Report',
     descriptor: CustomPropertyDescriptor<ReportExportContext, any>({
         name: 'pdbe_structure_quality_report',
         cifExport: {
diff --git a/src/mol-model-props/pdbe/themes/structure-quality-report.ts b/src/mol-model-props/pdbe/themes/structure-quality-report.ts
index e3296bab946d7dc478f50b5b20c3cfe05a533ef4..5f624e0f26a1944fb890826b3b7a72a8cee56ec0 100644
--- a/src/mol-model-props/pdbe/themes/structure-quality-report.ts
+++ b/src/mol-model-props/pdbe/themes/structure-quality-report.ts
@@ -72,13 +72,14 @@ export function StructureQualityReportColorTheme(ctx: ThemeDataContext, props: P
         granularity: 'group',
         color: color,
         props: props,
-        description: 'Assigns residue colors according to the number of issues or a specific issue in the PDBe Validation Report.',
+        description: 'Assigns residue colors according to the number of quality issues or a specific quality issue. Data from wwPDB Validation Report, obtained via PDBe.',
         legend: TableLegend(ValidationColorTable)
     }
 }
 
 export const StructureQualityReportColorThemeProvider: ColorTheme.Provider<Params> =  {
-    label: 'PDBe Structure Quality Report',
+    label: 'Structure Quality Report',
+    category: ColorTheme.Category.Validation,
     factory: StructureQualityReportColorTheme,
     getParams: ctx => {
         const issueTypes = StructureQualityReport.getIssueTypes(ctx.structure);
diff --git a/src/mol-model-props/rcsb/assembly-symmetry.ts b/src/mol-model-props/rcsb/assembly-symmetry.ts
index 6dce3e871e3ebd414256ecc7fb2af5825fe524f6..09a370b2ecd52b8a91fafcb91b73a5e9654190bf 100644
--- a/src/mol-model-props/rcsb/assembly-symmetry.ts
+++ b/src/mol-model-props/rcsb/assembly-symmetry.ts
@@ -15,6 +15,7 @@ import { CustomProperty } from '../common/custom-property';
 import { NonNullableArray } from '../../mol-util/type-helpers';
 import { CustomStructureProperty } from '../common/custom-structure-property';
 import { MmcifFormat } from '../../mol-model-formats/structure/mmcif';
+import { ReadonlyVec3 } from '../../mol-math/linear-algebra/3d/vec3';
 
 const BiologicalAssemblyNames = new Set([
     'author_and_software_defined_assembly',
@@ -26,6 +27,11 @@ const BiologicalAssemblyNames = new Set([
 ])
 
 export namespace AssemblySymmetry {
+    export enum Tag {
+        Cluster = 'rcsb-assembly-symmetry-cluster',
+        Representation = 'rcsb-assembly-symmetry-3d'
+    }
+
     export const DefaultServerUrl = 'https://data-beta.rcsb.org/graphql'
 
     export function isApplicable(structure?: Structure): boolean {
@@ -37,6 +43,7 @@ export namespace AssemblySymmetry {
         const mmcif = structure.models[0].sourceData.data.db
         if (!mmcif.pdbx_struct_assembly.details.isDefined) return false
         const id = structure.units[0].conformation.operator.assembly.id
+        if (id === '' || id === 'deposited') return true
         const indices = Column.indicesOf(mmcif.pdbx_struct_assembly.id, e => e === id)
         if (indices.length !== 1) return false
         const details = mmcif.pdbx_struct_assembly.details.value(indices[0])
@@ -48,27 +55,35 @@ export namespace AssemblySymmetry {
 
         const client = new GraphQLClient(props.serverUrl, ctx.fetch)
         const variables: AssemblySymmetryQueryVariables = {
-            assembly_id: structure.units[0].conformation.operator.assembly.id,
+            assembly_id: structure.units[0].conformation.operator.assembly.id || 'deposited',
             entry_id: structure.units[0].model.entryId
         }
         const result = await client.request<AssemblySymmetryQuery>(ctx.runtime, query, variables)
 
         if (!result.assembly?.rcsb_struct_symmetry) {
-            throw new Error('missing fields')
+            console.error('expected `rcsb_struct_symmetry` field')
+            return []
         }
         return result.assembly.rcsb_struct_symmetry as AssemblySymmetryValue
     }
+
+    export type RotationAxes = ReadonlyArray<{ order: number, start: ReadonlyVec3, end: ReadonlyVec3 }>
+    export function isRotationAxes(x: AssemblySymmetryValue[0]['rotation_axes']): x is RotationAxes {
+        return !!x && x.length > 0
+    }
 }
 
 export function getSymmetrySelectParam(structure?: Structure) {
-    const param = PD.Select<number>(0, [[0, 'No Symmetries']])
+    const param = PD.Select<number>(-1, [[-1, 'No Symmetries']])
     if (structure) {
         const assemblySymmetry = AssemblySymmetryProvider.get(structure).value
         if (assemblySymmetry) {
             const options: [number, string][] = []
             for (let i = 0, il = assemblySymmetry.length; i < il; ++i) {
                 const { symbol, kind } = assemblySymmetry[i]
-                options.push([ i, `${i + 1}: ${symbol} ${kind}` ])
+                if (symbol !== 'C1') {
+                    options.push([ i, `${i + 1}: ${symbol} ${kind}` ])
+                }
             }
             if (options.length) {
                 param.options = options
diff --git a/src/mol-model-props/rcsb/representations/assembly-symmetry.ts b/src/mol-model-props/rcsb/representations/assembly-symmetry.ts
index b2c2d994f281bc48e7d52175fdc73f0e2e427969..b18fc0b27b51ac96c22d0337d13bb6f3e56e7e29 100644
--- a/src/mol-model-props/rcsb/representations/assembly-symmetry.ts
+++ b/src/mol-model-props/rcsb/representations/assembly-symmetry.ts
@@ -5,7 +5,7 @@
  */
 
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
-import { AssemblySymmetryValue, getSymmetrySelectParam, AssemblySymmetryProvider } from '../assembly-symmetry';
+import { AssemblySymmetryValue, getSymmetrySelectParam, AssemblySymmetryProvider, AssemblySymmetry } from '../assembly-symmetry';
 import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
 import { Vec3, Mat4 } from '../../../mol-math/linear-algebra';
 import { addCylinder } from '../../../mol-geo/geometry/mesh/builder/cylinder';
@@ -29,7 +29,6 @@ import { TetrahedronCage } from '../../../mol-geo/primitive/tetrahedron';
 import { IcosahedronCage } from '../../../mol-geo/primitive/icosahedron';
 import { degToRad, radToDeg } from '../../../mol-math/misc';
 import { Mutable } from '../../../mol-util/type-helpers';
-import { ReadonlyVec3 } from '../../../mol-math/linear-algebra/3d/vec3';
 import { equalEps } from '../../../mol-math/linear-algebra/3d/common';
 import { Structure } from '../../../mol-model/structure';
 import { isInteger } from '../../../mol-util/number';
@@ -87,11 +86,6 @@ export type AssemblySymmetryProps = PD.Values<AssemblySymmetryParams>
 
 //
 
-type RotationAxes = ReadonlyArray<{ order: number, start: ReadonlyVec3, end: ReadonlyVec3 }>
-function isRotationAxes(x: AssemblySymmetryValue[0]['rotation_axes']): x is RotationAxes {
-    return !!x && x.length > 0
-}
-
 function getAssemblyName(s: Structure) {
     const { id } = s.units[0].conformation.operator.assembly
     return isInteger(id) ? `Assembly ${id}` : id
@@ -122,7 +116,7 @@ function getAxesMesh(data: AssemblySymmetryValue, props: PD.Values<AxesParams>,
     const { symmetryIndex, scale } = props
 
     const { rotation_axes } = data[symmetryIndex]
-    if (!isRotationAxes(rotation_axes)) return Mesh.createEmpty(mesh)
+    if (!AssemblySymmetry.isRotationAxes(rotation_axes)) return Mesh.createEmpty(mesh)
 
     const { start, end } = rotation_axes[0]
     const radius = (Vec3.distance(start, end) / 500) * scale
@@ -227,11 +221,11 @@ function getSymbolScale(symbol: string) {
     return 1
 }
 
-function setSymbolTransform(t: Mat4, symbol: string, axes: RotationAxes, size: number, structure: Structure) {
+function setSymbolTransform(t: Mat4, symbol: string, axes: AssemblySymmetry.RotationAxes, size: number, structure: Structure) {
     const eye = Vec3()
     const target = Vec3()
     const up = Vec3()
-    let pair: Mutable<RotationAxes> | undefined = undefined
+    let pair: Mutable<AssemblySymmetry.RotationAxes> | undefined = undefined
 
     if (symbol.startsWith('C')) {
         pair = [axes[0]]
@@ -288,7 +282,7 @@ function getCageMesh(data: Structure, props: PD.Values<CageParams>, mesh?: Mesh)
     const { symmetryIndex, scale } = props
 
     const { rotation_axes, symbol } = assemblySymmetry[symmetryIndex]
-    if (!isRotationAxes(rotation_axes)) return Mesh.createEmpty(mesh)
+    if (!AssemblySymmetry.isRotationAxes(rotation_axes)) return Mesh.createEmpty(mesh)
 
     const cage = getSymbolCage(symbol)
     if (!cage) return Mesh.createEmpty(mesh)
@@ -329,5 +323,5 @@ function getCageShape(ctx: RuntimeContext, data: Structure, props: AssemblySymme
 
 export type AssemblySymmetryRepresentation = Representation<Structure, AssemblySymmetryParams>
 export function AssemblySymmetryRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, AssemblySymmetryParams>): AssemblySymmetryRepresentation {
-    return Representation.createMulti('Symmetry', ctx, getParams, Representation.StateBuilder, AssemblySymmetryVisuals as unknown as Representation.Def<Structure, AssemblySymmetryParams>)
+    return Representation.createMulti('Assembly Symmetry', ctx, getParams, Representation.StateBuilder, AssemblySymmetryVisuals as unknown as Representation.Def<Structure, AssemblySymmetryParams>)
 }
\ No newline at end of file
diff --git a/src/mol-model-props/rcsb/representations/validation-report-clashes.ts b/src/mol-model-props/rcsb/representations/validation-report-clashes.ts
index be69dbcda1645c51fda6b31ec0768f2cc15460c4..7f05f21e29f49b49b3192d30de8fa4003c9d6b82 100644
--- a/src/mol-model-props/rcsb/representations/validation-report-clashes.ts
+++ b/src/mol-model-props/rcsb/representations/validation-report-clashes.ts
@@ -279,8 +279,8 @@ export function ClashesRepresentation(ctx: RepresentationContext, getParams: Rep
 }
 
 export const ClashesRepresentationProvider: StructureRepresentationProvider<ClashesParams> = {
-    label: 'RCSB Clashes',
-    description: 'Displays clashes between atoms as disks.',
+    label: 'Validation Clashes',
+    description: 'Displays clashes between atoms as disks. Data from wwPDB Validation Report, obtained via RCSB PDB.',
     factory: ClashesRepresentation,
     getParams: getClashesParams,
     defaultValues: PD.getDefaultValues(ClashesParams),
diff --git a/src/mol-model-props/rcsb/themes/assembly-symmetry-cluster.ts b/src/mol-model-props/rcsb/themes/assembly-symmetry-cluster.ts
index e32a62f46d426ad45635add7173fcc7039fd5b8e..5ef15770e2c16dd1b724849768f0cbb4e46d8494 100644
--- a/src/mol-model-props/rcsb/themes/assembly-symmetry-cluster.ts
+++ b/src/mol-model-props/rcsb/themes/assembly-symmetry-cluster.ts
@@ -59,11 +59,13 @@ export function AssemblySymmetryClusterColorTheme(ctx: ThemeDataContext, props:
             for (let j = 0, jl = members.length; j < jl; ++j) {
                 const asymId = members[j]!.asym_id
                 const operList = [...members[j]!.pdbx_struct_oper_list_ids || []] as string[]
-                if (operList.length === 0) operList.push('1') // TODO hack assuming '1' is the id of the identity operator
                 clusterByMember.set(clusterMemberKey(asymId, operList), i)
+                if (operList.length === 0) {
+                    operList.push('1') // TODO hack assuming '1' is the id of the identity operator
+                    clusterByMember.set(clusterMemberKey(asymId, operList), i)
+                }
             }
         }
-
         const palette = getPalette(clusters.length, props)
         legend = palette.legend
 
@@ -84,13 +86,14 @@ export function AssemblySymmetryClusterColorTheme(ctx: ThemeDataContext, props:
         color,
         props,
         contextHash,
-        description: 'Assigns chain colors according to assembly symmetry cluster membership.',
+        description: 'Assigns chain colors according to assembly symmetry cluster membership calculated with BioJava and obtained via RCSB PDB.',
         legend
     }
 }
 
 export const AssemblySymmetryClusterColorThemeProvider: ColorTheme.Provider<AssemblySymmetryClusterColorThemeParams> = {
-    label: 'RCSB Assembly Symmetry Cluster',
+    label: 'Assembly Symmetry Cluster',
+    category: ColorTheme.Category.Symmetry,
     factory: AssemblySymmetryClusterColorTheme,
     getParams: getAssemblySymmetryClusterColorThemeParams,
     defaultValues: PD.getDefaultValues(AssemblySymmetryClusterColorThemeParams),
diff --git a/src/mol-model-props/rcsb/themes/density-fit.ts b/src/mol-model-props/rcsb/themes/density-fit.ts
index 9bc3d3d0fe4f8756e9483ad1b205e2966629a677..0661d73d228daf94cf144292aa26404113533b1c 100644
--- a/src/mol-model-props/rcsb/themes/density-fit.ts
+++ b/src/mol-model-props/rcsb/themes/density-fit.ts
@@ -55,13 +55,14 @@ export function DensityFitColorTheme(ctx: ThemeDataContext, props: {}): ColorThe
         color,
         props,
         contextHash,
-        description: 'Assigns residue colors according to the density fit using normalized Real Space R (RSRZ) for polymer residues and real space correlation coefficient (RSCC) for ligands. Colors range from poor (RSRZ = 2 or RSCC = 0.678) - to better (RSRZ = 0 or RSCC = 1.0).',
+        description: 'Assigns residue colors according to the density fit using normalized Real Space R (RSRZ) for polymer residues and real space correlation coefficient (RSCC) for ligands. Colors range from poor (RSRZ = 2 or RSCC = 0.678) - to better (RSRZ = 0 or RSCC = 1.0). Data from wwPDB Validation Report, obtained via RCSB PDB.',
         legend: scaleRsrz.legend
     }
 }
 
 export const DensityFitColorThemeProvider: ColorTheme.Provider<{}> = {
-    label: 'RCSB Density Fit',
+    label: 'Density Fit',
+    category: ColorTheme.Category.Validation,
     factory: DensityFitColorTheme,
     getParams: () => ({}),
     defaultValues: PD.getDefaultValues({}),
diff --git a/src/mol-model-props/rcsb/themes/geometry-quality.ts b/src/mol-model-props/rcsb/themes/geometry-quality.ts
index 1f39b8de4b19e47b1765c0c85ae93cbe7ab9c64f..834ccefab83e3a9807c063e41f1ab8749eac0fa6 100644
--- a/src/mol-model-props/rcsb/themes/geometry-quality.ts
+++ b/src/mol-model-props/rcsb/themes/geometry-quality.ts
@@ -95,13 +95,14 @@ export function GeometryQualityColorTheme(ctx: ThemeDataContext, props: PD.Value
         color,
         props,
         contextHash,
-        description: 'Assigns residue colors according to the number of (filtered) geometry issues.',
+        description: 'Assigns residue colors according to the number of (filtered) geometry issues. Data from wwPDB Validation Report, obtained via RCSB PDB.',
         legend: ColorLegend
     }
 }
 
 export const GeometryQualityColorThemeProvider: ColorTheme.Provider<GeometricQualityColorThemeParams> = {
-    label: 'RCSB Geometry Quality',
+    label: 'Geometry Quality',
+    category: ColorTheme.Category.Validation,
     factory: GeometryQualityColorTheme,
     getParams: getGeometricQualityColorThemeParams,
     defaultValues: PD.getDefaultValues(getGeometricQualityColorThemeParams({})),
diff --git a/src/mol-model-props/rcsb/themes/random-coil-index.ts b/src/mol-model-props/rcsb/themes/random-coil-index.ts
index 596aa205587e6fbbade46e08d205404414e226a5..1dedd686f457c9380f8b857046956a3200238fd1 100644
--- a/src/mol-model-props/rcsb/themes/random-coil-index.ts
+++ b/src/mol-model-props/rcsb/themes/random-coil-index.ts
@@ -46,13 +46,14 @@ export function RandomCoilIndexColorTheme(ctx: ThemeDataContext, props: {}): Col
         color,
         props,
         contextHash,
-        description: 'Assigns residue colors according to the Random Coil Index value.',
+        description: 'Assigns residue colors according to the Random Coil Index value. Data from wwPDB Validation Report, obtained via RCSB PDB.',
         legend: scale.legend
     }
 }
 
 export const RandomCoilIndexColorThemeProvider: ColorTheme.Provider<{}> = {
-    label: 'RCSB Random Coil Index',
+    label: 'Random Coil Index',
+    category: ColorTheme.Category.Validation,
     factory: RandomCoilIndexColorTheme,
     getParams: () => ({}),
     defaultValues: PD.getDefaultValues({}),
diff --git a/src/mol-model-props/rcsb/validation-report.ts b/src/mol-model-props/rcsb/validation-report.ts
index ada844656b5fd832507d19efae31e330cbe3d98c..307c05dc428375e0ecfe5da2a6a0e7ee95578170 100644
--- a/src/mol-model-props/rcsb/validation-report.ts
+++ b/src/mol-model-props/rcsb/validation-report.ts
@@ -19,6 +19,9 @@ import { arrayMax } from '../../mol-util/array';
 import { equalEps } from '../../mol-math/linear-algebra/3d/common';
 import { Vec3 } from '../../mol-math/linear-algebra';
 import { MmcifFormat } from '../../mol-model-formats/structure/mmcif';
+import { QuerySymbolRuntime } from '../../mol-script/runtime/query/compiler';
+import { CustomPropSymbol } from '../../mol-script/language/symbol';
+import Type from '../../mol-script/language/type';
 
 export { ValidationReport }
 
@@ -118,6 +121,25 @@ namespace ValidationReport {
             case 'server': return fetch(ctx, model, props.source.params)
         }
     }
+
+    export const symbols = {
+        hasClash: QuerySymbolRuntime.Dynamic(CustomPropSymbol('rcsb', 'validation-report.has-clash', Type.Bool),
+            ctx => {
+                const { unit, element } = ctx.element
+                if (!Unit.isAtomic(unit)) return 0
+                const validationReport = ValidationReportProvider.get(unit.model).value
+                return validationReport && validationReport.clashes.getVertexEdgeCount(element) > 0
+            }
+        ),
+        issueCount: QuerySymbolRuntime.Dynamic(CustomPropSymbol('rcsb', 'validation-report.issue-count', Type.Num),
+            ctx => {
+                const { unit, element } = ctx.element
+                if (!Unit.isAtomic(unit)) return 0
+                const validationReport = ValidationReportProvider.get(unit.model).value
+                return validationReport?.geometryIssues.get(unit.residueIndex[element])?.size || 0
+            }
+        ),
+    }
 }
 
 const FileSourceParams = {
@@ -140,10 +162,10 @@ export type ValidationReportParams = typeof ValidationReportParams
 export type ValidationReportProps = PD.Values<ValidationReportParams>
 
 export const ValidationReportProvider: CustomModelProperty.Provider<ValidationReportParams, ValidationReport> = CustomModelProperty.createProvider({
-    label: 'RCSB Validation Report',
+    label: 'Validation Report',
     descriptor: CustomPropertyDescriptor({
         name: 'rcsb_validation_report',
-        // TODO `cifExport` and `symbol`
+        symbols: ValidationReport.symbols
     }),
     type: 'dynamic',
     defaultParams: ValidationReportParams,
diff --git a/src/mol-model/loci.ts b/src/mol-model/loci.ts
index f80c1468a9566c1f8dc0bad747490c5545ee30a0..d8151534a663129aa1ba109219ad7d3f35d1ebca 100644
--- a/src/mol-model/loci.ts
+++ b/src/mol-model/loci.ts
@@ -42,7 +42,8 @@ export function isDataLoci(x?: Loci): x is DataLoci {
     return !!x && x.kind === 'data-loci';
 }
 export function areDataLociEqual(a: DataLoci, b: DataLoci) {
-    if (a.data !== b.data || a.tag !== b.tag) return false
+    // use shallowEqual to allow simple data objects that are contructed on-the-fly
+    if (!shallowEqual(a.data, b.data) || a.tag !== b.tag) return false
     if (a.elements.length !== b.elements.length) return false
     for (let i = 0, il = a.elements.length; i < il; ++i) {
         if (!shallowEqual(a.elements[i], b.elements[i])) return false
diff --git a/src/mol-model/sequence/sequence.ts b/src/mol-model/sequence/sequence.ts
index 946e953b38d0873a8687647bbf1bed559805d86c..5e381e198a1da2eefc611c1754a53514b799bcbb 100644
--- a/src/mol-model/sequence/sequence.ts
+++ b/src/mol-model/sequence/sequence.ts
@@ -80,11 +80,11 @@ namespace Sequence {
         return code
     }
 
-    export function ofResidueNames(compId: Column<string>, seqId: Column<number>, modifiedMap?: ReadonlyMap<string, string>): Sequence {
+    export function ofResidueNames(compId: Column<string>, seqId: Column<number>): Sequence {
         if (seqId.rowCount === 0) throw new Error('cannot be empty');
 
         const kind = determineKind(compId);
-        return new ResidueNamesImpl(kind, compId, seqId, modifiedMap) as Sequence;
+        return new ResidueNamesImpl(kind, compId, seqId) as Sequence;
     }
 
     class ResidueNamesImpl<K extends Kind, Alphabet extends string> implements Base<K, Alphabet> {
@@ -154,11 +154,7 @@ namespace Sequence {
                 const code = this.codeFromName(name);
                 // in case of MICROHETEROGENEITY `sequenceArray[idx]` may already be set
                 if (!sequenceArray[idx] || sequenceArray[idx] === '-') {
-                    if (code === 'X' && this.modifiedMap && this.modifiedMap.has(name)) {
-                        sequenceArray[idx] = this.modifiedMap.get(name)!
-                    } else {
-                        sequenceArray[idx] = code;
-                    }
+                    sequenceArray[idx] = code;
                 }
                 labels[idx].push(code === 'X' ? name : code);
                 compIds[seqId].push(name);
@@ -183,7 +179,7 @@ namespace Sequence {
             this._length = count
         }
 
-        constructor(public kind: K, public compId: Column<string>, public seqId: Column<number>, private modifiedMap?: ReadonlyMap<string, string>) {
+        constructor(public kind: K, public compId: Column<string>, public seqId: Column<number>) {
 
             this.codeFromName = codeProvider(kind)
         }
diff --git a/src/mol-model/structure/export/categories/modified-residues.ts b/src/mol-model/structure/export/categories/modified-residues.ts
deleted file mode 100644
index 51726e8c6b2a1df02b78335e1b9087f7c01b3a33..0000000000000000000000000000000000000000
--- a/src/mol-model/structure/export/categories/modified-residues.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * Copyright (c) 2017-2018 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 { Segmentation } from '../../../../mol-data/int';
-import { CifWriter } from '../../../../mol-io/writer/cif';
-import { StructureElement, StructureProperties as P, Unit } from '../../../structure';
-import { CifExportContext } from '../mmcif';
-
-import CifField = CifWriter.Field
-import CifCategory = CifWriter.Category
-
-const pdbx_struct_mod_residue_fields: CifField<number, StructureElement.Location[]>[] = [
-    CifField.index('id'),
-    CifField.str(`label_comp_id`, (i, xs) => P.residue.label_comp_id(xs[i])),
-    CifField.int(`label_seq_id`, (i, xs) => P.residue.label_seq_id(xs[i])),
-    CifField.str(`pdbx_PDB_ins_code`, (i, xs) => P.residue.pdbx_PDB_ins_code(xs[i])),
-    CifField.str(`label_asym_id`, (i, xs) => P.chain.label_asym_id(xs[i])),
-    CifField.str(`label_entity_id`, (i, xs) => P.chain.label_entity_id(xs[i])),
-    CifField.str(`auth_comp_id`, (i, xs) => P.residue.auth_comp_id(xs[i])),
-    CifField.int(`auth_seq_id`, (i, xs) => P.residue.auth_seq_id(xs[i])),
-    CifField.str(`auth_asym_id`, (i, xs) => P.chain.auth_asym_id(xs[i])),
-    CifField.str<number, StructureElement.Location[]>('parent_comp_id', (i, xs) => xs[i].unit.model.properties.modifiedResidues.parentId.get(P.residue.label_comp_id(xs[i]))!),
-    CifField.str('details', (i, xs) => xs[i].unit.model.properties.modifiedResidues.details.get(P.residue.label_comp_id(xs[i]))!)
-];
-
-function getModifiedResidues({ structures }: CifExportContext): StructureElement.Location[] {
-    // TODO: can different models (in the same mmCIF file) have different modified residues?
-    const structure = structures[0], model = structure.model;
-    const map = model.properties.modifiedResidues.parentId;
-    if (!map.size) return [];
-
-    const ret = [];
-    const prop = P.residue.label_comp_id;
-    const loc = StructureElement.Location.create(structure);
-    for (const unit of structure.units) {
-        if (!Unit.isAtomic(unit) || !unit.conformation.operator.isIdentity) continue;
-        const residues = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements);
-        loc.unit = unit;
-        while (residues.hasNext) {
-            const seg = residues.move();
-            loc.element = unit.elements[seg.start];
-            const name = prop(loc);
-            if (map.has(name)) {
-                ret[ret.length] = StructureElement.Location.clone(loc);
-            }
-        }
-    }
-    return ret;
-}
-
-export const _pdbx_struct_mod_residue: CifCategory<CifExportContext> = {
-    name: 'pdbx_struct_mod_residue',
-    instance(ctx) {
-        const residues = getModifiedResidues(ctx);
-        return {
-            fields: pdbx_struct_mod_residue_fields,
-            source: [{ data: residues, rowCount: residues.length }]
-        };
-    }
-}
\ No newline at end of file
diff --git a/src/mol-model/structure/export/mmcif.ts b/src/mol-model/structure/export/mmcif.ts
index 45071c82cddc16fb1654c76e1e69595d1bef0622..c90223bb94e5df514d24d19cb64df12c4b04cb4d 100644
--- a/src/mol-model/structure/export/mmcif.ts
+++ b/src/mol-model/structure/export/mmcif.ts
@@ -11,7 +11,6 @@ import { Structure } from '../structure'
 import { _atom_site } from './categories/atom_site';
 import CifCategory = CifWriter.Category
 import { _struct_conf, _struct_sheet_range } from './categories/secondary-structure';
-import { _pdbx_struct_mod_residue } from './categories/modified-residues';
 import { _chem_comp, _pdbx_chem_comp_identifier, _pdbx_nonpoly_scheme } from './categories/misc';
 import { Model } from '../model';
 import { getUniqueEntityIndicesFromStructures, copy_mmCif_category } from './categories/utils';
@@ -83,7 +82,6 @@ const Categories = [
     copy_mmCif_category('atom_sites'),
 
     _pdbx_nonpoly_scheme,
-    _pdbx_struct_mod_residue,
 
     // Atoms
     _atom_site
diff --git a/src/mol-model/structure/model/model.ts b/src/mol-model/structure/model/model.ts
index 04dd2a0dd482c28713a5bb4c68686bf244752153..220fa825faa923cf9cdcfd042940213d4248b7e6 100644
--- a/src/mol-model/structure/model/model.ts
+++ b/src/mol-model/structure/model/model.ts
@@ -9,7 +9,7 @@ import UUID from '../../../mol-util/uuid';
 import StructureSequence from './properties/sequence';
 import { AtomicHierarchy, AtomicConformation, AtomicRanges } from './properties/atomic';
 import { CoarseHierarchy, CoarseConformation } from './properties/coarse';
-import { Entities, ChemicalComponentMap, MissingResidues } from './properties/common';
+import { Entities, ChemicalComponentMap, MissingResidues, StructAsymMap } from './properties/common';
 import { CustomProperties } from '../common/custom-property';
 import { SaccharideComponentMap } from '../structure/carbohydrates/constants';
 import { ModelFormat } from '../../../mol-model-formats/structure/format';
@@ -53,17 +53,14 @@ export interface Model extends Readonly<{
     atomicRanges: AtomicRanges,
 
     properties: {
-        /** maps modified residue name to its parent */
-        readonly modifiedResidues: Readonly<{
-            parentId: ReadonlyMap<string, string>,
-            details: ReadonlyMap<string, string>
-        }>,
         /** map that holds details about unobserved or zero occurrence residues */
         readonly missingResidues: MissingResidues,
         /** maps residue name to `ChemicalComponent` data */
         readonly chemicalComponentMap: ChemicalComponentMap
         /** maps residue name to `SaccharideComponent` data */
         readonly saccharideComponentMap: SaccharideComponentMap
+        /** maps label_asym_id name to `StructAsym` data */
+        readonly structAsymMap: StructAsymMap
     },
 
     customProperties: CustomProperties,
diff --git a/src/mol-model/structure/model/properties/common.ts b/src/mol-model/structure/model/properties/common.ts
index 84809978a4f22079e51edfbaca0f7b1433aff234..f8ae77e6b3869499d0eae32c3db939d6c92a8111 100644
--- a/src/mol-model/structure/model/properties/common.ts
+++ b/src/mol-model/structure/model/properties/common.ts
@@ -29,4 +29,7 @@ export interface MissingResidues {
     has(model_num: number, asym_id: string, seq_id: number): boolean
     get(model_num: number, asym_id: string, seq_id: number): MissingResidue | undefined
     readonly size: number
-}
\ No newline at end of file
+}
+
+export type StructAsym = Table.Row<Pick<mmCIF_Schema['struct_asym'], 'id' | 'entity_id'> & { auth_id: Column.Schema.Str }>
+export type StructAsymMap = ReadonlyMap<string, StructAsym>
\ No newline at end of file
diff --git a/src/mol-model/structure/model/properties/sequence.ts b/src/mol-model/structure/model/properties/sequence.ts
index 1e8b787b0bd4c7f2d50a185066df1022fcb36938..f05dd175091aad6396e32c882c774333924597e6 100644
--- a/src/mol-model/structure/model/properties/sequence.ts
+++ b/src/mol-model/structure/model/properties/sequence.ts
@@ -37,13 +37,13 @@ namespace StructureSequence {
         return { sequences, byEntityKey }
     }
 
-    export function fromHierarchy(entities: Entities, atomicHierarchy: AtomicHierarchy, coarseHierarchy: CoarseHierarchy, modResMap?: ReadonlyMap<string, string>): StructureSequence {
-        const atomic = fromAtomicHierarchy(entities, atomicHierarchy, modResMap)
+    export function fromHierarchy(entities: Entities, atomicHierarchy: AtomicHierarchy, coarseHierarchy: CoarseHierarchy): StructureSequence {
+        const atomic = fromAtomicHierarchy(entities, atomicHierarchy)
         const coarse = coarseHierarchy.isDefined ? fromCoarseHierarchy(entities, coarseHierarchy) : Empty
         return merge(atomic, coarse)
     }
 
-    export function fromAtomicHierarchy(entities: Entities, hierarchy: AtomicHierarchy, modResMap?: ReadonlyMap<string, string>): StructureSequence {
+    export function fromAtomicHierarchy(entities: Entities, hierarchy: AtomicHierarchy): StructureSequence {
         const { label_comp_id, label_seq_id } = hierarchy.residues
         const { chainAtomSegments, residueAtomSegments } = hierarchy
         const { count, offsets } = chainAtomSegments
@@ -75,7 +75,7 @@ namespace StructureSequence {
             const num = Column.window(label_seq_id, rStart, rEnd);
             byEntityKey[entityKey] = {
                 entityId: entities.data.id.value(entityKey),
-                sequence: Sequence.ofResidueNames(compId, num, modResMap)
+                sequence: Sequence.ofResidueNames(compId, num)
             };
 
             sequences.push(byEntityKey[entityKey]);
diff --git a/src/mol-model/structure/model/types.ts b/src/mol-model/structure/model/types.ts
index 95a1e8e41d6b9688c79b8316186748eb08501001..fd37efe1d499a5c0193eac441e13ad09cdbc80e3 100644
--- a/src/mol-model/structure/model/types.ts
+++ b/src/mol-model/structure/model/types.ts
@@ -137,7 +137,7 @@ export const PolymerTypeAtomRoleId: { [k in PolymerType]: { [k in AtomRole]: Set
 export const ProteinBackboneAtoms = new Set([
     'CA', 'C', 'N', 'O',
     'O1', 'O2', 'OC1', 'OC2', 'OX1', 'OXT',
-    'H', 'H1', 'H2', 'H3', 'HA', 'HN',
+    'H', 'H1', 'H2', 'H3', 'HA', 'HN', 'HXT',
     'BB'
 ])
 
@@ -257,7 +257,7 @@ export const DnaBaseNames = new Set([
     'DN' // unknown DNA base from CCD
 ])
 export const PeptideBaseNames = new Set([ 'APN', 'CPN', 'TPN', 'GPN' ])
-export const PurineBaseNames = new Set([ 'A', 'G', 'DA', 'DG', 'DI', 'APN', 'GPN' ])
+export const PurineBaseNames = new Set([ 'A', 'G', 'I', 'DA', 'DG', 'DI', 'APN', 'GPN' ])
 export const PyrimidineBaseNames = new Set([ 'C', 'T', 'U', 'DC', 'DT', 'DU', 'CPN', 'TPN' ])
 export const BaseNames = SetUtils.unionMany(RnaBaseNames, DnaBaseNames, PeptideBaseNames)
 
@@ -342,7 +342,7 @@ export function getDefaultChemicalComponent(compId: string): ChemicalComponent {
         formula_weight: 0,
         id: compId,
         name: compId,
-        mon_nstd_flag: 'n',
+        mon_nstd_flag: PolymerNames.has(compId) ? 'y' : 'n',
         pdbx_synonyms: [],
         type: getComponentType(compId)
     };
diff --git a/src/mol-model/structure/structure/element/loci.ts b/src/mol-model/structure/structure/element/loci.ts
index 76746ddb8b49af4dcda61173166e9c1c6d6df908..ffe5a13d5afa36d7c701cdfa54d8915fee2b555c 100644
--- a/src/mol-model/structure/structure/element/loci.ts
+++ b/src/mol-model/structure/structure/element/loci.ts
@@ -98,6 +98,24 @@ export namespace Loci {
         return Location.create(loci.structure, unit, element);
     }
 
+    export function firstElement(loci: Loci): Loci {
+        if (isEmpty(loci)) return loci;
+        return Loci(loci.structure, [{
+            unit: loci.elements[0].unit,
+            indices: OrderedSet.ofSingleton(OrderedSet.start(loci.elements[0].indices))
+        }])
+    }
+
+    export function firstResidue(loci: Loci): Loci {
+        if (isEmpty(loci)) return loci;
+        return extendToWholeResidues(firstElement(loci))
+    }
+
+    export function firstChain(loci: Loci): Loci {
+        if (isEmpty(loci)) return loci;
+        return extendToWholeChains(firstElement(loci))
+    }
+
     export function toStructure(loci: Loci): Structure {
         const units: Unit[] = []
         for (const e of loci.elements) {
diff --git a/src/mol-model/structure/structure/properties.ts b/src/mol-model/structure/structure/properties.ts
index 48ab835a516d3c50613ad4e5004389db0f6d53a8..6890b2a742c7e14f48d490ecbaebfa0ef1e3e017 100644
--- a/src/mol-model/structure/structure/properties.ts
+++ b/src/mol-model/structure/structure/properties.ts
@@ -1,14 +1,15 @@
 /**
- * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2020 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 StructureElement from './element'
 import Unit from './unit'
 import { VdwRadius } from '../model/properties/atomic';
-import { ModelSecondaryStructure } from '../../../mol-model-formats/structure/property/secondary-structure';
 import { SecondaryStructureType } from '../model/types';
+import { SecondaryStructureProvider } from '../../../mol-model-props/computed/secondary-structure';
 
 function p<T>(p: StructureElement.Property<T>) { return p; }
 
@@ -96,28 +97,18 @@ const residue = {
     pdbx_PDB_ins_code: p(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.residues.pdbx_PDB_ins_code.value(l.unit.residueIndex[l.element])),
 
     // Properties
-    isModified: p(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.properties.modifiedResidues.parentId.has(compId(l))),
-    modifiedParentName: p(l => {
-        if (!Unit.isAtomic(l.unit)) notAtomic()
-        const id = compId(l)
-        return l.unit.model.properties.modifiedResidues.parentId.get(id) || id
-    }),
     isNonStandard: p(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.properties.chemicalComponentMap.get(compId(l))!.mon_nstd_flag[0] !== 'y'),
     hasMicroheterogeneity: p(hasMicroheterogeneity),
     microheterogeneityCompIds: p(microheterogeneityCompIds),
-    // TODO implement as symbol in SecondaryStructureProvider (not ModelSecondaryStructure.Provider)
     secondary_structure_type: p(l => {
         if (!Unit.isAtomic(l.unit)) notAtomic()
-        const secondaryStructure = ModelSecondaryStructure.Provider.get(l.unit.model)
-        if (secondaryStructure) return secondaryStructure.type[l.unit.residueIndex[l.element]]
-        else return SecondaryStructureType.Flag.NA
+        const secStruc = SecondaryStructureProvider.get(l.structure).value?.get(l.unit.id)
+        return secStruc?.type[l.unit.residueIndex[l.element]] ?? SecondaryStructureType.Flag.NA
     }),
-    // TODO implement as symbol in SecondaryStructureProvider (not ModelSecondaryStructure.Provider)
     secondary_structure_key: p(l => {
         if (!Unit.isAtomic(l.unit)) notAtomic()
-        const secondaryStructure = ModelSecondaryStructure.Provider.get(l.unit.model)
-        if (secondaryStructure) return secondaryStructure.key[l.unit.residueIndex[l.element]]
-        else return -1
+        const secStruc = SecondaryStructureProvider.get(l.structure).value?.get(l.unit.id)
+        return secStruc?.key[l.unit.residueIndex[l.element]] ?? -1
     }),
     chem_comp_type: p(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.properties.chemicalComponentMap.get(compId(l))!.type),
 }
diff --git a/src/mol-model/structure/structure/structure.ts b/src/mol-model/structure/structure/structure.ts
index c03a2a97b212e350f5bcf609fc421b98bb2036d1..aac52cad54c5f86a8c3b55734057d100fd6178f0 100644
--- a/src/mol-model/structure/structure/structure.ts
+++ b/src/mol-model/structure/structure/structure.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2020 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>
@@ -16,7 +16,6 @@ import { StructureLookup3D } from './util/lookup3d';
 import { CoarseElements } from '../model/properties/coarse';
 import { StructureSubsetBuilder } from './util/subset-builder';
 import { InterUnitBonds, computeInterUnitBonds, Bond } from './unit/bonds';
-import { PairRestraints, CrossLinkRestraint, extractCrossLinkRestraints } from './unit/pair-restraints';
 import StructureSymmetry from './symmetry';
 import StructureProperties from './properties';
 import { ResidueIndex, ChainIndex, EntityIndex } from '../model/indexing';
@@ -41,7 +40,6 @@ class Structure {
         parent?: Structure,
         lookup3d?: StructureLookup3D,
         interUnitBonds?: InterUnitBonds,
-        crossLinkRestraints?: PairRestraints<CrossLinkRestraint>,
         unitSymmetryGroups?: ReadonlyArray<Unit.SymmetryGroup>,
         unitSymmetryGroupsIndexMap?: IntMap<number>,
         carbohydrates?: Carbohydrates,
@@ -228,12 +226,6 @@ class Structure {
         return this._props.interUnitBonds;
     }
 
-    get crossLinkRestraints() {
-        if (this._props.crossLinkRestraints) return this._props.crossLinkRestraints;
-        this._props.crossLinkRestraints = extractCrossLinkRestraints(this);
-        return this._props.crossLinkRestraints;
-    }
-
     get unitSymmetryGroups(): ReadonlyArray<Unit.SymmetryGroup> {
         if (this._props.unitSymmetryGroups) return this._props.unitSymmetryGroups;
         this._props.unitSymmetryGroups = StructureSymmetry.computeTransformGroups(this);
diff --git a/src/mol-model/structure/structure/symmetry.ts b/src/mol-model/structure/structure/symmetry.ts
index e6c5bae4673f2379bfaeb1b2296f905b74613b70..40df81d6ed48db7c2190141b34a06a9a26476a46 100644
--- a/src/mol-model/structure/structure/symmetry.ts
+++ b/src/mol-model/structure/structure/symmetry.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2020 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>
@@ -11,10 +11,11 @@ import { Spacegroup, SpacegroupCell, SymmetryOperator } from '../../../mol-math/
 import { Vec3, Mat4 } from '../../../mol-math/linear-algebra';
 import { RuntimeContext, Task } from '../../../mol-task';
 import { Symmetry, Model } from '../model';
-import { QueryContext, StructureSelection } from '../query';
+import { QueryContext, StructureSelection, Queries as Q } from '../query';
 import Structure from './structure';
 import Unit from './unit';
 import { ModelSymmetry } from '../../../mol-model-formats/structure/property/symmetry';
+import StructureProperties from './properties';
 
 namespace StructureSymmetry {
     export function buildAssembly(structure: Structure, asmName: string) {
@@ -48,6 +49,40 @@ namespace StructureSymmetry {
         });
     }
 
+    export type Generators = { operators: { index: number, shift: Vec3 }[], asymIds: string[] }[]
+
+    export function buildSymmetryAssembly(structure: Structure, generators: Generators, symmetry: Symmetry) {
+        return Task.create('Build Symmetry Assembly', async ctx => {
+            const models = structure.models;
+            if (models.length !== 1) throw new Error('Can only build symmetry assemblies from structures based on 1 model.');
+
+            const modelCenter = Vec3()
+            const assembler = Structure.Builder({ label: structure.label });
+
+            const queryCtx = new QueryContext(structure);
+
+            for (const g of generators) {
+                const selector = getSelector(g.asymIds);
+                const selection = selector(queryCtx);
+                if (StructureSelection.structureCount(selection) === 0) {
+                    continue;
+                }
+                const { units } = StructureSelection.unionStructure(selection);
+
+                for (const { index, shift: [i, j, k] } of g.operators) {
+                    const operators = getOperatorsForIndex(symmetry, index, i, j, k, modelCenter)
+                    for (const unit of units) {
+                        for (const op of operators) {
+                            assembler.addWithOperator(unit, op);
+                        }
+                    }
+                }
+            }
+
+            return assembler.getStructure();
+        });
+    }
+
     export function builderSymmetryMates(structure: Structure, radius: number) {
         return Task.create('Find Symmetry Mates', ctx => findMatesRadius(ctx, structure, radius));
     }
@@ -96,7 +131,35 @@ namespace StructureSymmetry {
     }
 }
 
-function getOperators(symmetry: Symmetry, ijkMin: Vec3, ijkMax: Vec3, modelCenter: Vec3) {
+function getSelector(asymIds: string[]) {
+    return Q.generators.atoms({ chainTest: Q.pred.and(
+        Q.pred.eq(ctx => StructureProperties.unit.operator_name(ctx.element), SymmetryOperator.DefaultName),
+        Q.pred.inSet(ctx => StructureProperties.chain.label_asym_id(ctx.element), asymIds)
+    )});
+}
+
+function getOperatorsForIndex(symmetry: Symmetry, index: number, i: number, j: number, k: number, modelCenter: Vec3) {
+    const { spacegroup, ncsOperators } = symmetry;
+    const operators: SymmetryOperator[] = []
+
+    const { toFractional } = spacegroup.cell
+    const ref = Vec3.transformMat4(Vec3(), modelCenter, toFractional)
+
+    const symOp = Spacegroup.getSymmetryOperatorRef(spacegroup, index, i, j, k, ref)
+    if (ncsOperators && ncsOperators.length) {
+        for (let u = 0, ul = ncsOperators.length; u < ul; ++u) {
+            const ncsOp = ncsOperators![u]
+            const matrix = Mat4.mul(Mat4(), symOp.matrix, ncsOp.matrix)
+            const operator = SymmetryOperator.create(`${symOp.name} ${ncsOp.name}`, matrix, symOp.assembly, ncsOp.ncsId, symOp.hkl, symOp.spgrOp);
+            operators.push(operator)
+        }
+    } else {
+        operators.push(symOp)
+    }
+    return operators
+}
+
+function getOperatorsForRange(symmetry: Symmetry, ijkMin: Vec3, ijkMax: Vec3, modelCenter: Vec3) {
     const { spacegroup, ncsOperators } = symmetry;
     const ncsCount = (ncsOperators && ncsOperators.length) || 0
     const operators: SymmetryOperator[] = [];
@@ -117,18 +180,7 @@ function getOperators(symmetry: Symmetry, ijkMin: Vec3, ijkMax: Vec3, modelCente
                 for (let k = ijkMin[2]; k <= ijkMax[2]; k++) {
                     // check if we have added identity as the 1st operator.
                     if (!ncsCount && op === 0 && i === 0 && j === 0 && k === 0) continue;
-
-                    const symOp = Spacegroup.getSymmetryOperatorRef(spacegroup, op, i, j, k, ref)
-                    if (ncsCount) {
-                        for (let u = 0; u < ncsCount; ++u) {
-                            const ncsOp = ncsOperators![u]
-                            const matrix = Mat4.mul(Mat4.zero(), symOp.matrix, ncsOp.matrix)
-                            const operator = SymmetryOperator.create(`${symOp.name} ${ncsOp.name}`, matrix, symOp.assembly, ncsOp.ncsId, symOp.hkl, symOp.spgrOp);
-                            operators[operators.length] = operator;
-                        }
-                    } else {
-                        operators[operators.length] = symOp;
-                    }
+                    operators.push(...getOperatorsForIndex(symmetry, op, i, j, k, ref))
                 }
             }
         }
@@ -142,7 +194,7 @@ function getOperatorsCached333(symmetry: Symmetry, ref: Vec3) {
     }
     symmetry._operators_333 = {
         ref: Vec3.clone(ref),
-        operators: getOperators(symmetry, Vec3.create(-3, -3, -3), Vec3.create(3, 3, 3), ref)
+        operators: getOperatorsForRange(symmetry, Vec3.create(-3, -3, -3), Vec3.create(3, 3, 3), ref)
     };
     return symmetry._operators_333.operators;
 }
@@ -181,7 +233,7 @@ async function findSymmetryRange(ctx: RuntimeContext, structure: Structure, ijkM
     if (SpacegroupCell.isZero(spacegroup.cell)) return structure;
 
     const modelCenter = Model.getCenter(models[0])
-    const operators = getOperators(symmetry, ijkMin, ijkMax, modelCenter);
+    const operators = getOperatorsForRange(symmetry, ijkMin, ijkMax, modelCenter);
     return assembleOperators(structure, operators);
 }
 
diff --git a/src/mol-model/structure/structure/unit/pair-restraints.ts b/src/mol-model/structure/structure/unit/pair-restraints.ts
deleted file mode 100644
index 890fb6b03666a40e5157d80430f0b098d2db98ad..0000000000000000000000000000000000000000
--- a/src/mol-model/structure/structure/unit/pair-restraints.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-export * from './pair-restraints/data'
-export * from './pair-restraints/extract-cross-links'
-// export * from './pair-restraints/extract-predicted-contacts'
-// export * from './pair-restraints/extract-distance-restraints'
diff --git a/src/mol-model/structure/structure/unit/pair-restraints/data.ts b/src/mol-model/structure/structure/unit/pair-restraints/data.ts
deleted file mode 100644
index f402aab8e991b6fbc7a03e4bdc4fb38fbb9ef622..0000000000000000000000000000000000000000
--- a/src/mol-model/structure/structure/unit/pair-restraints/data.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import Unit from '../../unit';
-import { StructureElement } from '../../../structure';
-
-const emptyArray: number[] = []
-
-interface PairRestraint {
-    readonly unitA: Unit,
-    readonly unitB: Unit,
-    readonly indexA: StructureElement.UnitIndex,
-    readonly indexB: StructureElement.UnitIndex,
-}
-
-function getPairKey(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit) {
-    return `${indexA}|${unitA.id}|${indexB}|${unitB.id}`
-}
-
-export class PairRestraints<T extends PairRestraint> {
-    readonly count: number
-    private readonly pairKeyIndices: Map<string, number[]>
-
-    /** Indices into this.pairs */
-    getPairIndices(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit): ReadonlyArray<number> {
-        const key = getPairKey(indexA, unitA, indexB, unitB)
-        const indices = this.pairKeyIndices.get(key)
-        return indices !== undefined ? indices : emptyArray
-    }
-
-    getPairs(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit): T[] | undefined {
-        const indices = this.getPairIndices(indexA, unitA, indexB, unitB)
-        return indices.length ? indices.map(idx => this.pairs[idx]) : undefined
-    }
-
-    constructor(public pairs: ReadonlyArray<T>) {
-        const pairKeyIndices = new Map<string, number[]>()
-        this.pairs.forEach((p, i) => {
-            const key = getPairKey(p.indexA, p.unitA, p.indexB, p.unitB)
-            const indices = pairKeyIndices.get(key)
-            if (indices) indices.push(i)
-            else pairKeyIndices.set(key, [i])
-        })
-
-        this.count = pairs.length
-        this.pairKeyIndices = pairKeyIndices
-    }
-}
-
-export interface CrossLinkRestraint extends PairRestraint {
-    readonly restraintType: 'harmonic' | 'upper bound' | 'lower bound'
-    readonly distanceThreshold: number
-    readonly psi: number
-    readonly sigma1: number
-    readonly sigma2: number
-}
-
-export interface PredictedContactRestraint extends PairRestraint {
-    readonly distance_lower_limit: number
-    readonly distance_upper_limit: number
-    readonly probability: number
-    readonly restraint_type: 'lower bound' | 'upper bound' | 'lower and upper bound'
-    readonly model_granularity: 'by-residue' | 'by-feature' | 'by-atom'
-}
-
-export interface DistanceRestraint extends PairRestraint {
-    readonly upper_limit: number
-    readonly upper_limit_esd: number
-    readonly lower_limit: number
-    readonly lower_limit_esd: number
-    readonly probability: number
-    readonly restraint_type: 'lower bound' | 'upper bound' | 'lower and upper bound'
-    readonly granularity: 'by-residue' | 'by-atom'
-}
\ No newline at end of file
diff --git a/src/mol-model/structure/structure/unit/pair-restraints/extract-cross-links.ts b/src/mol-model/structure/structure/unit/pair-restraints/extract-cross-links.ts
deleted file mode 100644
index 0b98c288cd7e44f5409655d1919e232a726fdb0b..0000000000000000000000000000000000000000
--- a/src/mol-model/structure/structure/unit/pair-restraints/extract-cross-links.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-/**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import Unit from '../../unit';
-import Structure from '../../structure';
-import { PairRestraints, CrossLinkRestraint } from './data';
-import { StructureElement } from '../../../structure';
-import { ModelCrossLinkRestraint } from '../../../../../mol-model-formats/structure/property/pair-restraints/cross-links';
-
-function _addRestraints(map: Map<number, number>, unit: Unit, restraints: ModelCrossLinkRestraint) {
-    const { elements } = unit;
-    const elementCount = elements.length;
-    const kind = unit.kind
-
-    for (let i = 0; i < elementCount; i++) {
-        const e = elements[i];
-        restraints.getIndicesByElement(e, kind).forEach(ri => map.set(ri, i))
-    }
-}
-
-function extractInter(pairs: CrossLinkRestraint[], unitA: Unit, unitB: Unit) {
-    if (unitA.model !== unitB.model) return
-    if (unitA.model.sourceData.kind !== 'mmCIF') return
-
-    const restraints = ModelCrossLinkRestraint.Provider.get(unitA.model)
-    if (!restraints) return
-
-    const rA = new Map<number, StructureElement.UnitIndex>();
-    const rB = new Map<number, StructureElement.UnitIndex>();
-    _addRestraints(rA, unitA, restraints)
-    _addRestraints(rB, unitB, restraints)
-
-    rA.forEach((indexA, ri) => {
-        const indexB = rB.get(ri)
-        if (indexB !== undefined) {
-            pairs.push(
-                createCrossLinkRestraint(unitA, indexA, unitB, indexB, restraints, ri),
-                createCrossLinkRestraint(unitB, indexB, unitA, indexA, restraints, ri)
-            )
-        }
-    })
-}
-
-function extractIntra(pairs: CrossLinkRestraint[], unit: Unit) {
-    if (unit.model.sourceData.kind !== 'mmCIF') return
-
-    const restraints = ModelCrossLinkRestraint.Provider.get(unit.model)
-    if (!restraints) return
-
-    const { elements } = unit;
-    const elementCount = elements.length;
-    const kind = unit.kind
-
-    const r = new Map<number, StructureElement.UnitIndex[]>();
-
-    for (let i = 0; i < elementCount; i++) {
-        const e = elements[i];
-        restraints.getIndicesByElement(e, kind).forEach(ri => {
-            const il = r.get(ri)
-            if (il) il.push(i as StructureElement.UnitIndex)
-            else r.set(ri, [i as StructureElement.UnitIndex])
-        })
-    }
-
-    r.forEach((il, ri) => {
-        if (il.length < 2) return
-        const [ indexA, indexB ] = il
-        pairs.push(
-            createCrossLinkRestraint(unit, indexA, unit, indexB, restraints, ri),
-            createCrossLinkRestraint(unit, indexB, unit, indexA, restraints, ri)
-        )
-    })
-}
-
-function createCrossLinkRestraint(unitA: Unit, indexA: StructureElement.UnitIndex, unitB: Unit, indexB: StructureElement.UnitIndex, restraints: ModelCrossLinkRestraint, row: number): CrossLinkRestraint {
-    return {
-        unitA, indexA, unitB, indexB,
-
-        restraintType: restraints.data.restraint_type.value(row),
-        distanceThreshold: restraints.data.distance_threshold.value(row),
-        psi: restraints.data.psi.value(row),
-        sigma1: restraints.data.sigma_1.value(row),
-        sigma2: restraints.data.sigma_2.value(row),
-    }
-}
-
-function extractCrossLinkRestraints(structure: Structure): PairRestraints<CrossLinkRestraint> {
-    const pairs: CrossLinkRestraint[] = []
-    if (!structure.models.some(m => ModelCrossLinkRestraint.Provider.get(m))) {
-        return new PairRestraints(pairs)
-    }
-
-    const n = structure.units.length
-    for (let i = 0; i < n; ++i) {
-        const unitA = structure.units[i]
-        extractIntra(pairs, unitA)
-        for (let j = i + 1; j < n; ++j) {
-            const unitB = structure.units[j]
-            if (unitA.model === unitB.model) {
-                extractInter(pairs, unitA, unitB)
-            }
-        }
-    }
-
-    return new PairRestraints(pairs)
-}
-
-export { extractCrossLinkRestraints };
diff --git a/src/mol-model/structure/structure/unit/pair-restraints/extract-distance-restraints.ts b/src/mol-model/structure/structure/unit/pair-restraints/extract-distance-restraints.ts
deleted file mode 100644
index f19797cb6a8dc5eed5dacaaf0201d24d458fba44..0000000000000000000000000000000000000000
--- a/src/mol-model/structure/structure/unit/pair-restraints/extract-distance-restraints.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-// TODO extract from `_ma_distance_restraints`
\ No newline at end of file
diff --git a/src/mol-model/structure/structure/unit/pair-restraints/extract-predicted-contacts.ts b/src/mol-model/structure/structure/unit/pair-restraints/extract-predicted-contacts.ts
deleted file mode 100644
index ad6d43f51f4d524171b5d66a2c19f4d0348c45cb..0000000000000000000000000000000000000000
--- a/src/mol-model/structure/structure/unit/pair-restraints/extract-predicted-contacts.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-// TODO extract from `ihm_predicted_contact_restraint`
\ No newline at end of file
diff --git a/src/mol-plugin-ui/base.tsx b/src/mol-plugin-ui/base.tsx
index d377417addc71ca14c8c4eec0c0cbccd37cdcc07..38d3977f1b6eecf3d819ab8e038ef5ea36cf048c 100644
--- a/src/mol-plugin-ui/base.tsx
+++ b/src/mol-plugin-ui/base.tsx
@@ -25,6 +25,7 @@ export abstract class PluginUIComponent<P = {}, S = {}, SS = {}> extends React.C
     componentWillUnmount() {
         if (!this.subs) return;
         for (const s of this.subs) s.unsubscribe();
+        this.subs = [];
     }
 
     protected init?(): void;
diff --git a/src/mol-plugin-ui/controls/action-menu.tsx b/src/mol-plugin-ui/controls/action-menu.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..016a4db71cf127f89be927c5dd5ec53885aa457f
--- /dev/null
+++ b/src/mol-plugin-ui/controls/action-menu.tsx
@@ -0,0 +1,157 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as React from 'react'
+import { Icon } from './common';
+import { ParamDefinition } from '../../mol-util/param-definition';
+
+export class ActionMenu extends React.PureComponent<ActionMenu.Props> {
+    hide = () => this.props.onSelect(void 0)
+
+    render() {
+        const cmd = this.props;
+
+        return <div className='msp-action-menu-options' style={{ marginTop: '1px' }}>
+            {cmd.header && <div className='msp-control-group-header' style={{ position: 'relative' }}>
+                <button className='msp-btn msp-btn-block' onClick={this.hide}>
+                    <Icon name='off' style={{ position: 'absolute', right: '2px', top: 0 }} />
+                    <b>{cmd.header}</b>
+                </button>
+            </div>}
+            <Section items={cmd.items} onSelect={cmd.onSelect} current={cmd.current} />
+        </div>
+    }
+}
+
+export namespace ActionMenu {
+    export type Props = { items: Items, onSelect: OnSelect, header?: string, current?: Item | undefined }
+
+    export type OnSelect = (item: Item | undefined) => void
+
+    export type Items = string | Item | [Items]
+    export type Item = { label: string, icon?: string, value: unknown }
+
+    export function Item(label: string, value: unknown): Item
+    export function Item(label: string, icon: string, value: unknown): Item
+    export function Item(label: string, iconOrValue: any, value?: unknown): Item {
+        if (value) return { label, icon: iconOrValue, value };
+        return { label, value: iconOrValue };
+    }
+
+    export function createItems<T>(xs: ArrayLike<T>, options?: { label?: (t: T) => string, value?: (t: T) => any, category?: (t: T) => string | undefined }) {
+        const { label, value, category } = options || { };
+        let cats: Map<string, (ActionMenu.Item | string)[]> | undefined = void 0;
+        const items: (ActionMenu.Item | (ActionMenu.Item | string)[] | string)[] = [];
+        for (let i = 0; i < xs.length; i++) {
+            const x = xs[i];
+
+            const catName = category?.(x);
+            const l = label ? label(x) : '' + x;
+            const v = value ? value(x) : x;
+
+            if (!!catName) {
+                if (!cats) cats = new Map<string, (ActionMenu.Item | string)[]>();
+
+                let cat = cats.get(catName);
+                if (!cat) {
+                    cat = [catName];
+                    cats.set(catName, cat);
+                    items.push(cat);
+                }
+                cat.push(ActionMenu.Item(l, v));
+            } else {
+                items.push(ActionMenu.Item(l, v));
+            }
+        }
+        return items as ActionMenu.Items;
+    }
+    
+    type Opt = ParamDefinition.Select<any>['options'][0];
+    const _selectOptions = { value: (o: Opt) => o[0], label: (o: Opt) => o[1], category: (o: Opt) => o[2] };
+
+    export function createItemsFromSelectParam(param: ParamDefinition.Select<any>) {
+        return createItems(param.options, _selectOptions);
+    }
+
+    export function findItem(items: Items, value: any): Item | undefined {
+        if (typeof items === 'string') return;
+        if (isItem(items)) return items.value === value ? items : void 0;
+        for (const s of items) {
+            const found = findItem(s, value);
+            if (found) return found;
+        }
+    }
+
+    export function getFirstItem(items: Items): Item | undefined {
+        if (typeof items === 'string') return;
+        if (isItem(items)) return items;
+        for (const s of items) {
+            const found = getFirstItem(s);
+            if (found) return found;
+        }
+    }
+}
+
+type SectionProps = { header?: string, items: ActionMenu.Items, onSelect: ActionMenu.OnSelect, current: ActionMenu.Item | undefined }
+type SectionState = { items: ActionMenu.Items, current: ActionMenu.Item | undefined, isExpanded: boolean }
+
+class Section extends React.PureComponent<SectionProps, SectionState> {
+    state = {
+        items: this.props.items,
+        current: this.props.current,
+        isExpanded: !!this.props.current && !!ActionMenu.findItem(this.props.items, this.props.current.value)
+    }
+
+    toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
+        this.setState({ isExpanded: !this.state.isExpanded });
+        e.currentTarget.blur();
+    }
+
+    static getDerivedStateFromProps(props: SectionProps, state: SectionState) {
+        if (props.items === state.items && props.current === state.current) return null;
+        return { items: props.items, current: props.current, isExpanded: props.current && !!ActionMenu.findItem(props.items, props.current.value) }
+    }
+
+    render() {
+        const { header, items, onSelect, current } = this.props;
+
+        if (typeof items === 'string') return null;
+        if (isItem(items)) return <Action item={items} onSelect={onSelect} current={current} />
+
+        const hasCurrent = header && current && !!ActionMenu.findItem(items, current.value)
+
+        return <div>
+            {header && <div className='msp-control-group-header' style={{ marginTop: '1px' }}>
+                <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}>
+                    <span className={`msp-icon msp-icon-${this.state.isExpanded ? 'collapse' : 'expand'}`} />
+                    {hasCurrent ? <b>{header}</b> : header}
+                </button>
+            </div>}
+            <div className='msp-control-offset'>
+                {(!header || this.state.isExpanded) && items.map((x, i) => {
+                    if (typeof x === 'string') return null;
+                    if (isItem(x)) return <Action key={i} item={x} onSelect={onSelect} current={current} />
+                    return <Section key={i} header={typeof x[0] === 'string' ? x[0] : void 0} items={x} onSelect={onSelect} current={current} />
+                })}
+            </div>
+        </div>;
+    }
+}
+
+const Action: React.FC<{ item: ActionMenu.Item, onSelect: ActionMenu.OnSelect, current: ActionMenu.Item | undefined }> = ({ item, onSelect, current }) => {
+    const isCurrent = current === item;
+    return <div className='msp-control-row'>
+        <button onClick={() => onSelect(item)}>
+            {item.icon && <Icon name={item.icon} />}
+            {isCurrent ? <b>{item.label}</b> : item.label}
+        </button>
+    </div>;
+}
+
+function isItem(x: any): x is ActionMenu.Item {
+    const v = x as ActionMenu.Item;
+    return v && !!v.label && typeof v.value !== 'undefined';
+}
\ No newline at end of file
diff --git a/src/mol-plugin-ui/controls/common.tsx b/src/mol-plugin-ui/controls/common.tsx
index c345eb5d86c0a1c0e90f8e6c7f8590700a6d5576..8faadb834ce81d0f303727627e77c69cf6a68513 100644
--- a/src/mol-plugin-ui/controls/common.tsx
+++ b/src/mol-plugin-ui/controls/common.tsx
@@ -308,16 +308,28 @@ export function SectionHeader(props: { icon?: string, title: string | JSX.Elemen
     </div>
 }
 
-// export const ToggleButton = (props: {
-//     onChange: (v: boolean) => void,
-//     value: boolean,
-//     label: string,
-//     title?: string
-// }) => <div className='lm-control-row lm-toggle-button' title={props.title}>
-//         <span>{props.label}</span>
-//         <div>
-//             <button onClick={e => { props.onChange.call(null, !props.value); (e.target as HTMLElement).blur(); }}>
-//                     <span className={ `lm-icon lm-icon-${props.value ? 'ok' : 'off'}` }></span> {props.value ? 'On' : 'Off'}
-//             </button>
-//         </div>
-//     </div>
\ No newline at end of file
+export type ToggleButtonProps = {
+    style?: React.CSSProperties,
+    className?: string,
+    disabled?: boolean,
+    label: string | JSX.Element,
+    title?: string,
+    isSelected?: boolean,
+    toggle: () => void
+}
+
+export class ToggleButton extends React.PureComponent<ToggleButtonProps> {
+    onClick = (e: React.MouseEvent<HTMLButtonElement>) => {
+        e.currentTarget.blur();
+        this.props.toggle();
+    }
+
+    render() {
+        const props = this.props;
+        const label = props.label;
+        return <button onClick={this.onClick} title={this.props.title}
+            disabled={props.disabled} style={props.style} className={props.className}>
+            {this.props.isSelected ? <b>{label}</b> : label}
+        </button>;
+    }
+}
\ No newline at end of file
diff --git a/src/mol-plugin-ui/controls/parameters.tsx b/src/mol-plugin-ui/controls/parameters.tsx
index 49363d5cd68a1fa21a03855f4ffe426dd887a1a4..ddb081b50dd7dc3449f0fc282d76f4cd980aafed 100644
--- a/src/mol-plugin-ui/controls/parameters.tsx
+++ b/src/mol-plugin-ui/controls/parameters.tsx
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 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>
@@ -8,27 +8,33 @@
 import { Vec2, Vec3 } from '../../mol-math/linear-algebra';
 import { Color } from '../../mol-util/color';
 import { ColorListName, getColorListFromName } from '../../mol-util/color/lists';
-import { memoize1 } from '../../mol-util/memoize';
+import { memoize1, memoizeLatest } from '../../mol-util/memoize';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { camelCaseToWords } from '../../mol-util/string';
 import * as React from 'react';
 import LineGraphComponent from './line-graph/line-graph-component';
 import { Slider, Slider2 } from './slider';
-import { NumericInput, IconButton, ControlGroup } from './common';
-import { _Props, _State } from '../base';
+import { NumericInput, IconButton, ControlGroup, ToggleButton } from './common';
+import { _Props, _State, PluginUIComponent } from '../base';
 import { legendFor } from './legend';
 import { Legend as LegendData } from '../../mol-util/legend';
 import { CombinedColorControl, ColorValueOption, ColorOptions } from './color';
+import { getPrecision } from '../../mol-util/number';
+import { ParamMapping } from '../../mol-util/param-mapping';
+import { PluginContext } from '../../mol-plugin/context';
+import { ActionMenu } from './action-menu';
 
 export interface ParameterControlsProps<P extends PD.Params = PD.Params> {
     params: P,
     values: any,
-    onChange: ParamOnChange,
+    onChange: ParamsOnChange<PD.Values<P>>,
     isDisabled?: boolean,
     onEnter?: () => void
 }
 
 export class ParameterControls<P extends PD.Params> extends React.PureComponent<ParameterControlsProps<P>, {}> {
+    onChange: ParamOnChange = (params) => this.props.onChange(params, this.props.values);
+
     render() {
         const params = this.props.params;
         const values = this.props.values;
@@ -40,12 +46,31 @@ export class ParameterControls<P extends PD.Params> extends React.PureComponent<
                 if (param.isHidden) return null;
                 const Control = controlFor(param);
                 if (!Control) return null;
-                return <Control param={param} key={key} onChange={this.props.onChange} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} name={key} value={values[key]} />
+                return <Control param={param} key={key} onChange={this.onChange} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} name={key} value={values[key]} />
             })}
         </>;
     }
 }
 
+export class ParameterMappingControl<S, T> extends PluginUIComponent<{ mapping: ParamMapping<S, T, PluginContext> }> {
+    setSettings = (p: { param: PD.Base<any>, name: string, value: any }, old: any) => {
+        const values = { ...old, [p.name]: p.value };
+        const t = this.props.mapping.update(values, this.plugin);
+        this.props.mapping.apply(t, this.plugin);
+    }
+
+    componentDidMount() {
+        this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
+    }
+
+    render() {
+        const t = this.props.mapping.getTarget(this.plugin);
+        const values = this.props.mapping.getValues(t, this.plugin);
+        const params = this.props.mapping.params(this.plugin) as any as PD.Params;
+        return <ParameterControls params={params} values={values} onChange={this.setSettings} />
+    }
+}
+
 function controlFor(param: PD.Any): ParamControl | undefined {
     switch (param.type) {
         case 'value': return void 0;
@@ -90,6 +115,7 @@ export class ParamHelp<L extends LegendData> extends React.PureComponent<{ legen
     }
 }
 
+export type ParamsOnChange<P> = (params: { param: PD.Base<any>, name: string, value: any }, values: Readonly<P>) => void
 export type ParamOnChange = (params: { param: PD.Base<any>, name: string, value: any }) => void
 export interface ParamProps<P extends PD.Base<any> = PD.Base<any>> {
     name: string,
@@ -101,51 +127,63 @@ export interface ParamProps<P extends PD.Base<any> = PD.Base<any>> {
 }
 export type ParamControl = React.ComponentClass<ParamProps<any>>
 
-export abstract class SimpleParam<P extends PD.Any> extends React.PureComponent<ParamProps<P>, { isExpanded: boolean }> {
-    state = { isExpanded: false };
+function renderSimple(options: { props: ParamProps<any>, state: { showHelp: boolean }, control: JSX.Element, addOn: JSX.Element | null, toggleHelp: () => void }) {
+    const { props, state, control, toggleHelp, addOn } = options;
+
+    const _className = ['msp-control-row'];
+    if (props.param.shortLabel) _className.push('msp-control-label-short')
+    if (props.param.twoColumns) _className.push('msp-control-col-2')
+    const className = _className.join(' ');
+
+    const label = props.param.label || camelCaseToWords(props.name);
+    const help = props.param.help
+        ? props.param.help(props.value)
+        : { description: props.param.description, legend: props.param.legend }
+    const desc = props.param.description;
+    const hasHelp = help.description || help.legend
+    return <>
+        <div className={className}>
+            <span title={desc}>
+                {label}
+                {hasHelp &&
+                    <button className='msp-help msp-btn-link msp-btn-icon msp-control-group-expander' onClick={toggleHelp}
+                        title={desc || `${state.showHelp ? 'Hide' : 'Show'} help`}
+                        style={{ background: 'transparent', textAlign: 'left', padding: '0' }}>
+                        <span className={`msp-icon msp-icon-help-circle-${state.showHelp ? 'collapse' : 'expand'}`} />
+                    </button>
+                }
+            </span>
+            <div>
+                {control}
+            </div>
+        </div>
+        {hasHelp && state.showHelp && <div className='msp-control-offset'>
+            <ParamHelp legend={help.legend} description={help.description} />
+        </div>}
+        {addOn}
+    </>;
+}
+
+export abstract class SimpleParam<P extends PD.Any> extends React.PureComponent<ParamProps<P>, { showHelp: boolean }> {
+    state = { showHelp: false };
 
     protected update(value: P['defaultValue']) {
         this.props.onChange({ param: this.props.param, name: this.props.name, value });
     }
 
     abstract renderControl(): JSX.Element;
+    renderAddOn(): JSX.Element | null { return null; }
 
-    private get className() {
-        const className = ['msp-control-row'];
-        if (this.props.param.shortLabel) className.push('msp-control-label-short')
-        if (this.props.param.twoColumns) className.push('msp-control-col-2')
-        return className.join(' ')
-    }
-
-    toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded });
+    toggleHelp = () => this.setState({ showHelp: !this.state.showHelp });
 
     render() {
-        const label = this.props.param.label || camelCaseToWords(this.props.name);
-        const help = this.props.param.help
-            ? this.props.param.help(this.props.value)
-            : { description: this.props.param.description, legend: this.props.param.legend }
-        const desc = this.props.param.description;
-        const hasHelp = help.description || help.legend
-        return <>
-            <div className={this.className}>
-                <span title={desc}>
-                    {label}
-                    {hasHelp &&
-                        <button className='msp-help msp-btn-link msp-btn-icon msp-control-group-expander' onClick={this.toggleExpanded}
-                            title={desc || `${this.state.isExpanded ? 'Hide' : 'Show'} help`}
-                            style={{ background: 'transparent', textAlign: 'left', padding: '0' }}>
-                            <span className={`msp-icon msp-icon-help-circle-${this.state.isExpanded ? 'collapse' : 'expand'}`} />
-                        </button>
-                    }
-                </span>
-                <div>
-                    {this.renderControl()}
-                </div>
-            </div>
-            {hasHelp && this.state.isExpanded && <div className='msp-control-offset'>
-                <ParamHelp legend={help.legend} description={help.description} />
-            </div>}
-        </>;
+        return renderSimple({
+            props: this.props,
+            state: this.state,
+            control: this.renderControl(),
+            toggleHelp: this.toggleHelp,
+            addOn: this.renderAddOn()
+        });
     }
 }
 
@@ -214,17 +252,20 @@ export class NumberInputControl extends React.PureComponent<ParamProps<PD.Numeri
     state = { value: '0' };
 
     update = (value: number) => {
+        const p = getPrecision(this.props.param.step || 0.01)
+        value = parseFloat(value.toFixed(p))
         this.props.onChange({ param: this.props.param, name: this.props.name, value });
     }
 
     render() {
         const placeholder = this.props.param.label || camelCaseToWords(this.props.name);
         const label = this.props.param.label || camelCaseToWords(this.props.name);
+        const p = getPrecision(this.props.param.step || 0.01)
         return <div className='msp-control-row'>
             <span title={this.props.param.description}>{label}</span>
             <div>
                 <NumericInput
-                    value={this.props.value} onEnter={this.props.onEnter} placeholder={placeholder}
+                    value={parseFloat(this.props.value.toFixed(p))} onEnter={this.props.onEnter} placeholder={placeholder}
                     isDisabled={this.props.isDisabled} onChange={this.update} />
             </div>
         </div>;
@@ -289,27 +330,97 @@ export class PureSelectControl extends  React.PureComponent<ParamProps<PD.Select
     }
 }
 
-export class SelectControl extends SimpleParam<PD.Select<string | number>> {
-    onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
-        if (typeof this.props.param.defaultValue === 'number') {
-            this.update(parseInt(e.target.value, 10));
+export class SelectControl extends React.PureComponent<ParamProps<PD.Select<string | number>>, { showHelp: boolean, showOptions: boolean }> {
+    state = { showHelp: false, showOptions: false };
+
+    onSelect: ActionMenu.OnSelect = item => {
+        if (!item || item.value === this.props.value) {
+            this.setState({ showOptions: false });
         } else {
-            this.update(e.target.value);
+            this.setState({ showOptions: false }, () => {
+                this.props.onChange({ param: this.props.param, name: this.props.name, value: item.value });
+            });
         }
     }
+
+    toggle = () => this.setState({ showOptions: !this.state.showOptions });
+
+    items = memoizeLatest((param: PD.Select<any>) => ActionMenu.createItemsFromSelectParam(param));
+
     renderControl() {
-        const isInvalid = this.props.value !== void 0 && !this.props.param.options.some(e => e[0] === this.props.value);
-        return <select value={this.props.value !== void 0 ? this.props.value : this.props.param.defaultValue} onChange={this.onChange} disabled={this.props.isDisabled}>
-            {isInvalid && <option key={this.props.value} value={this.props.value}>{`[Invalid] ${this.props.value}`}</option>}
-            {this.props.param.options.map(([value, label]) => <option key={value} value={value}>{label}</option>)}
-        </select>;
+        const items = this.items(this.props.param);
+        const current = this.props.value !== undefined ? ActionMenu.findItem(items, this.props.value) : void 0;
+        const label = current
+            ? current.label
+            : typeof this.props.value === 'undefined'
+            ? `${ActionMenu.getFirstItem(items)?.label || ''} [Default]`
+            : `[Invalid] ${this.props.value}`;
+        
+        return <ToggleButton disabled={this.props.isDisabled} style={{ textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis' }}
+            label={label} title={label as string} toggle={this.toggle} isSelected={this.state.showOptions} />;
+    }
+
+    renderAddOn() {
+        if (!this.state.showOptions) return null;
+
+        const items = this.items(this.props.param);
+        const current = ActionMenu.findItem(items, this.props.value);
+
+        return <ActionMenu items={items} current={current} onSelect={this.onSelect} />;
+    }
+
+    toggleHelp = () => this.setState({ showHelp: !this.state.showHelp });
+
+    render() {
+        return renderSimple({
+            props: this.props,
+            state: this.state,
+            control: this.renderControl(),
+            toggleHelp: this.toggleHelp,
+            addOn: this.renderAddOn()
+        });
     }
 }
 
-export class IntervalControl extends SimpleParam<PD.Interval> {
-    onChange = (v: [number, number]) => { this.update(v); }
-    renderControl() {
-        return <span>interval TODO</span>;
+export class IntervalControl extends React.PureComponent<ParamProps<PD.Interval>, { isExpanded: boolean }> {
+    state = { isExpanded: false }
+
+    components = {
+        0: PD.Numeric(0, { step: this.props.param.step }, { label: 'Min' }),
+        1: PD.Numeric(0, { step: this.props.param.step }, { label: 'Max' })
+    }
+
+    change(value: PD.MultiSelect<any>['defaultValue']) {
+        this.props.onChange({ name: this.props.name, param: this.props.param, value });
+    }
+
+    componentChange: ParamOnChange = ({ name, value }) => {
+        const v = [...this.props.value];
+        v[+name] = value;
+        this.change(v);
+    }
+
+    toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
+        this.setState({ isExpanded: !this.state.isExpanded });
+        e.currentTarget.blur();
+    }
+
+    render() {
+        const v = this.props.value;
+        const label = this.props.param.label || camelCaseToWords(this.props.name);
+        const p = getPrecision(this.props.param.step || 0.01)
+        const value = `[${v[0].toFixed(p)}, ${v[1].toFixed(p)}]`;
+        return <>
+            <div className='msp-control-row'>
+                <span>{label}</span>
+                <div>
+                    <button onClick={this.toggleExpanded}>{value}</button>
+                </div>
+            </div>
+            <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
+                <ParameterControls params={this.components} values={v} onChange={this.componentChange} onEnter={this.props.onEnter} />
+            </div>
+        </>;
     }
 }
 
@@ -399,9 +510,9 @@ export class Vec3Control extends React.PureComponent<ParamProps<PD.Vec3>, { isEx
     state = { isExpanded: false }
 
     components = {
-        0: PD.Numeric(0, void 0, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.x) || 'X' }),
-        1: PD.Numeric(0, void 0, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.y) || 'Y' }),
-        2: PD.Numeric(0, void 0, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.z) || 'Z' })
+        0: PD.Numeric(0, { step: this.props.param.step }, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.x) || 'X' }),
+        1: PD.Numeric(0, { step: this.props.param.step }, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.y) || 'Y' }),
+        2: PD.Numeric(0, { step: this.props.param.step }, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.z) || 'Z' })
     }
 
     change(value: PD.MultiSelect<any>['defaultValue']) {
@@ -422,7 +533,8 @@ export class Vec3Control extends React.PureComponent<ParamProps<PD.Vec3>, { isEx
     render() {
         const v = this.props.value;
         const label = this.props.param.label || camelCaseToWords(this.props.name);
-        const value = `[${v[0].toFixed(2)}, ${v[1].toFixed(2)}, ${v[2].toFixed(2)}]`;
+        const p = getPrecision(this.props.param.step || 0.01)
+        const value = `[${v[0].toFixed(p)}, ${v[1].toFixed(p)}, ${v[2].toFixed(p)}]`;
         return <>
             <div className='msp-control-row'>
                 <span>{label}</span>
@@ -529,7 +641,7 @@ export class MultiSelectControl extends React.PureComponent<ParamProps<PD.MultiS
     }
 }
 
-export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>, { isExpanded: boolean }> {
+export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>> & { inMapped?: boolean }, { isExpanded: boolean }> {
     state = { isExpanded: !!this.props.param.isExpanded }
 
     change(value: any) {
@@ -552,6 +664,10 @@ export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>,
 
         const controls = <ParameterControls params={params} onChange={this.onChangeParam} values={this.props.value} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />;
 
+        if (this.props.inMapped) {
+            return <div className='msp-control-offset'>{controls}</div>;
+        }
+
         if (this.props.param.isFlat) {
             return controls;
         }
@@ -570,7 +686,9 @@ export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>,
     }
 }
 
-export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>>> {
+export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>>, { isExpanded: boolean }> {
+    state = { isExpanded: false }
+
     private valuesCache: { [name: string]: PD.Values<any> } = {}
     private setValues(name: string, values: PD.Values<any>) {
         this.valuesCache[name] = values
@@ -596,6 +714,8 @@ export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>
         this.change({ name: this.props.value.name, params: e.value });
     }
 
+    toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded });
+
     render() {
         const value: PD.Mapped<any>['defaultValue'] = this.props.value;
         const param = this.props.param.map(value.name);
@@ -618,6 +738,14 @@ export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>
             return Select;
         }
 
+        if (param.type === 'group' && !param.isFlat && Object.keys(param.params).length > 0) {
+            return <div className='msp-mapped-parameter-group'>
+                {Select}
+                <IconButton icon='log' onClick={this.toggleExpanded} toggleState={this.state.isExpanded} title={`${label} Properties`} />
+                {this.state.isExpanded && <GroupControl inMapped param={param} value={value.params} name={`${label} Properties`} onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />}
+            </div>
+        }
+
         return <>
             {Select}
             <Mapped param={param} value={value.params} name={`${label} Properties`} onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />
diff --git a/src/mol-plugin-ui/skin/base/components/controls.scss b/src/mol-plugin-ui/skin/base/components/controls.scss
index 8a0f44a4cd5bf72828f1d4e5ce42f92b582f6c6f..725039cf8f025c6aae4781b2134ec1b1a5a9686c 100644
--- a/src/mol-plugin-ui/skin/base/components/controls.scss
+++ b/src/mol-plugin-ui/skin/base/components/controls.scss
@@ -31,6 +31,7 @@
         overflow: hidden;
         text-overflow: ellipsis;
         white-space: nowrap;
+        position: relative;
 
         @include non-selectable;
     }
diff --git a/src/mol-plugin-ui/skin/base/components/misc.scss b/src/mol-plugin-ui/skin/base/components/misc.scss
index 5208ebcec37b932de09e6fc9d5dc5103b51b38cd..a37bb0d4367c29a9d3a6fe8a3ed2fc31b6ff0709 100644
--- a/src/mol-plugin-ui/skin/base/components/misc.scss
+++ b/src/mol-plugin-ui/skin/base/components/misc.scss
@@ -166,4 +166,21 @@
     .msp-scrollable-container {
         left: $row-height + 1px;
     }
+}
+
+.msp-mapped-parameter-group {
+    position: relative;
+
+    > .msp-control-row:first-child {
+        > div:nth-child(2) {
+            right: 33px;
+        }
+    }
+
+    > .msp-btn-icon {
+        position: absolute;
+        right: 0;
+        width: 32px;
+        top: 0;
+    }
 }
\ No newline at end of file
diff --git a/src/mol-plugin-ui/skin/base/components/temp.scss b/src/mol-plugin-ui/skin/base/components/temp.scss
index 004a90f172d7f88a990b195e914aeb6ba1fda3e5..b614362ecbc4390fb85adbda052cbabca7ee8fc4 100644
--- a/src/mol-plugin-ui/skin/base/components/temp.scss
+++ b/src/mol-plugin-ui/skin/base/components/temp.scss
@@ -66,6 +66,7 @@
         text-align-last: center;
         background: none;
         padding: 0 $control-spacing;
+        overflow: hidden;
 
         > option[value = _] {
             display: none;
@@ -274,4 +275,34 @@
     .msp-transform-wrapper:last-child {
         margin-bottom: 10px;
     }
+}
+
+.msp-button-row {
+    display:flex;
+    flex-direction:row;
+    height: $row-height;
+    width: inherit;
+
+    > button {
+        margin: 0;
+        flex: 1 1 auto;
+        margin-right: 1px;
+        height: $row-height;
+
+        text-align-last: center;
+        background: none;
+        padding: 0 $control-spacing;
+        overflow: hidden;
+    }
+}
+
+.msp-action-menu-options {
+    .msp-control-row, button, .msp-icon {
+        height: 24px;
+        line-height: 24px;
+    }
+
+    button {
+        text-align: left;
+    }
 }
\ No newline at end of file
diff --git a/src/mol-plugin-ui/structure/representation.tsx b/src/mol-plugin-ui/structure/representation.tsx
index 2dcc444e1c6fb6f544967c3355526cb32ba2e121..bef7495baf6691e21d9856a7994657fd18883185 100644
--- a/src/mol-plugin-ui/structure/representation.tsx
+++ b/src/mol-plugin-ui/structure/representation.tsx
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -13,8 +13,6 @@ import { Color } from '../../mol-util/color';
 import { ButtonSelect, Options } from '../controls/common'
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { VisualQuality, VisualQualityOptions } from '../../mol-geo/geometry/base';
-import { StructureRepresentationPresets as P } from '../../mol-plugin/util/structure-representation-helper';
-import { camelCaseToWords } from '../../mol-util/string';
 import { CollapsableControls } from '../base';
 import { StateSelection, StateObject } from '../../mol-state';
 import { PluginStateObject } from '../../mol-plugin/state/objects';
@@ -133,13 +131,6 @@ export class StructureRepresentationControls extends CollapsableControls<Collaps
         this.subscribe(this.plugin.state.dataState.events.isUpdating, v => this.setState({ isDisabled: v }))
     }
 
-    preset = async (value: string) => {
-        const presetFn = P[value as keyof typeof P]
-        if (presetFn) {
-            await presetFn(this.plugin.helpers.structureRepresentation)
-        }
-    }
-
     onChange = async (p: { param: PD.Base<any>, name: string, value: any }) => {
         if (p.name === 'options') {
             await this.plugin.helpers.structureRepresentation.setIgnoreHydrogens(!p.value.showHydrogens)
@@ -178,15 +169,7 @@ export class StructureRepresentationControls extends CollapsableControls<Collaps
     }
 
     renderControls() {
-        const presets = PD.objectToOptions(P, camelCaseToWords);
         return <div>
-            <div className='msp-control-row'>
-                <div className='msp-select-row'>
-                    <ButtonSelect label='Preset' onChange={this.preset}>
-                        <optgroup label='Preset'>{Options(presets)}</optgroup>
-                    </ButtonSelect>
-                </div>
-            </div>
             <EverythingStructureRepresentationControls />
             <SelectionStructureRepresentationControls />
 
diff --git a/src/mol-plugin-ui/structure/selection.tsx b/src/mol-plugin-ui/structure/selection.tsx
index 8847ccc0b9e8154c62ce29936bed0e2e6c4bcae3..f7e380a84963e0cfea31e5e74c3aed2c9bc0bedd 100644
--- a/src/mol-plugin-ui/structure/selection.tsx
+++ b/src/mol-plugin-ui/structure/selection.tsx
@@ -1,28 +1,26 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author David Sehnal <david.sehnal@gmail.com>
  */
 
 import * as React from 'react';
 import { CollapsableControls, CollapsableState } from '../base';
-import { StructureSelectionQueries, SelectionModifier } from '../../mol-plugin/util/structure-selection-helper';
-import { ButtonSelect, Options } from '../controls/common';
+import { StructureSelectionQuery, SelectionModifier, StructureSelectionQueryList } from '../../mol-plugin/util/structure-selection-helper';
 import { PluginCommands } from '../../mol-plugin/command';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { Interactivity } from '../../mol-plugin/util/interactivity';
 import { ParameterControls } from '../controls/parameters';
 import { stripTags } from '../../mol-util/string';
 import { StructureElement } from '../../mol-model/structure';
+import { ActionMenu } from '../controls/action-menu';
+import { ToggleButton } from '../controls/common';
 
-const SSQ = StructureSelectionQueries
-const DefaultQueries: (keyof typeof SSQ)[] = [
-    'all', 'polymer', 'trace', 'backbone', 'protein', 'nucleic',
-    'helix', 'beta',
-    'water', 'branched', 'ligand', 'nonStandardPolymer',
-    'ring', 'aromaticRing',
-    'surroundings', 'complement', 'bonded'
-]
+export const DefaultQueries = ActionMenu.createItems(StructureSelectionQueryList, {
+    label: q => q.label,
+    category: q => q.category
+});
 
 const StructureSelectionParams = {
     granularity: Interactivity.Params.granularity,
@@ -33,7 +31,9 @@ interface StructureSelectionControlsState extends CollapsableState {
     extraRadius: number,
     durationMs: number,
 
-    isDisabled: boolean
+    isDisabled: boolean,
+
+    queryAction?: SelectionModifier
 }
 
 export class StructureSelectionControls<P, S extends StructureSelectionControlsState> extends CollapsableControls<P, S> {
@@ -46,7 +46,9 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
             this.forceUpdate()
         });
 
-        this.subscribe(this.plugin.state.dataState.events.isUpdating, v => this.setState({ isDisabled: v }))
+        this.subscribe(this.plugin.state.dataState.events.isUpdating, v => {
+            this.setState({ isDisabled: v, queryAction: void 0 })
+        })
     }
 
     get stats() {
@@ -115,38 +117,41 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
         }
     }
 
-    set = (modifier: SelectionModifier, value: string) => {
-        const query = SSQ[value as keyof typeof SSQ]
-        this.plugin.helpers.structureSelection.set(modifier, query.query, false)
-    }
-
-    add = (value: string) => this.set('add', value)
-    remove = (value: string) => this.set('remove', value)
-    only = (value: string) => this.set('only', value)
-
-    queries = Options(Object.keys(StructureSelectionQueries)
-            .map(name => [name, SSQ[name as keyof typeof SSQ].label] as [string, string])
-            .filter(pair => DefaultQueries.includes(pair[0] as keyof typeof SSQ)));
-
-    controls = <div className='msp-control-row'>
-        <div className='msp-select-row'>
-            <ButtonSelect label='Select' onChange={this.add} disabled={this.state.isDisabled}>
-                <optgroup label='Select'>
-                    {this.queries}
-                </optgroup>
-            </ButtonSelect>
-            <ButtonSelect label='Deselect' onChange={this.remove} disabled={this.state.isDisabled}>
-                <optgroup label='Deselect'>
-                    {this.queries}
-                </optgroup>
-            </ButtonSelect>
-            <ButtonSelect label='Only' onChange={this.only} disabled={this.state.isDisabled}>
-                <optgroup label='Only'>
-                    {this.queries}
-                </optgroup>
-            </ButtonSelect>
+    set = (modifier: SelectionModifier, selectionQuery: StructureSelectionQuery) => {
+        this.plugin.helpers.structureSelection.set(modifier, selectionQuery, false)
+    }
+
+    selectQuery: ActionMenu.OnSelect = item => {
+        if (!item || !this.state.queryAction) {
+            this.setState({ queryAction: void 0 });
+            return;
+        }
+        const q = this.state.queryAction!;
+        this.setState({ queryAction: void 0 }, () => {
+            this.set(q, item.value as StructureSelectionQuery);
+        })
+    }
+
+    queries = DefaultQueries
+
+    private showQueries(q: SelectionModifier) {
+        return () => this.setState({ queryAction: this.state.queryAction === q ? void 0 : q });
+    }
+
+    toggleAdd = this.showQueries('add')
+    toggleRemove = this.showQueries('remove')
+    toggleOnly = this.showQueries('only')
+
+    get controls() {
+        return <div>
+            <div className='msp-control-row msp-button-row'>
+                <ToggleButton label='Select' toggle={this.toggleAdd} isSelected={this.state.queryAction === 'add'} disabled={this.state.isDisabled} />
+                <ToggleButton label='Deselect' toggle={this.toggleRemove} isSelected={this.state.queryAction === 'remove'} disabled={this.state.isDisabled} />
+                <ToggleButton label='Only' toggle={this.toggleOnly} isSelected={this.state.queryAction === 'only'} disabled={this.state.isDisabled} />
+            </div>
+            {this.state.queryAction && <ActionMenu items={this.queries} onSelect={this.selectQuery} />}
         </div>
-    </div>
+    }
 
     defaultState() {
         return {
@@ -157,6 +162,8 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
             extraRadius: 4,
             durationMs: 250,
 
+            queryAction: void 0,
+
             isDisabled: false
         } as S
     }
diff --git a/src/mol-plugin-ui/viewport/simple-settings.tsx b/src/mol-plugin-ui/viewport/simple-settings.tsx
index 387e0154319e1b650725ccdc27eeaecbc10c2596..11c6b703f18c3bf991e0acbf09e81bd15f0c34b0 100644
--- a/src/mol-plugin-ui/viewport/simple-settings.tsx
+++ b/src/mol-plugin-ui/viewport/simple-settings.tsx
@@ -9,10 +9,23 @@ import * as React from 'react';
 import { Canvas3DParams } from '../../mol-canvas3d/canvas3d';
 import { PluginCommands } from '../../mol-plugin/command';
 import { ColorNames } from '../../mol-util/color/names';
-import { ParameterControls } from '../controls/parameters';
+import { ParameterMappingControl } from '../controls/parameters';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { PluginUIComponent } from '../base';
 import { Color } from '../../mol-util/color';
+import { ParamMapping } from '../../mol-util/param-mapping';
+import { PluginContext } from '../../mol-plugin/context';
+
+export class SimpleSettingsControl extends PluginUIComponent {
+    componentDidMount() {
+        this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
+    }
+
+    render() {
+        if (!this.plugin.canvas3d) return null;
+        return <ParameterMappingControl mapping={SimpleSettingsMapping} />
+    }
+}
 
 const SimpleSettingsParams = {
     spin: Canvas3DParams.trackball.params.spin,
@@ -21,80 +34,21 @@ const SimpleSettingsParams = {
         'transparent': PD.EmptyGroup(),
         'opaque': PD.Group({ color: PD.Color(Color(0xFCFBF9), { description: 'Custom background color' }) }, { isFlat: true })
     }, { description: 'Background of the 3D canvas' }),
-    renderStyle: PD.Select('glossy', [['flat', 'Flat'], ['matte', 'Matte'], ['glossy', 'Glossy'], ['metallic', 'Metallic']], { description: 'Style in which the 3D scene is rendered' }),
+    renderStyle: PD.Select('glossy', PD.arrayToOptions(['flat', 'matte', 'glossy', 'metallic']), { description: 'Style in which the 3D scene is rendered' }),
     occlusion: PD.Boolean(false, { description: 'Darken occluded crevices with the ambient occlusion effect' }),
     outline: PD.Boolean(false, { description: 'Draw outline around 3D objects' }),
     fog: PD.Boolean(false, { description: 'Show fog in the distance' }),
     clipFar: PD.Boolean(true, { description: 'Clip scene in the distance' }),
 };
 
-export class SimpleSettingsControl extends PluginUIComponent {
-    setSettings = (p: { param: PD.Base<any>, name: keyof typeof SimpleSettingsParams | string, value: any }) => {
-        if (p.name === 'spin') {
-            if (!this.plugin.canvas3d) return;
-            const trackball = this.plugin.canvas3d.props.trackball;
-            PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { trackball: { ...trackball, spin: p.value } } });
-        } else if (p.name === 'camera') {
-            PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { cameraMode: p.value }});
-        } else if (p.name === 'background') {
-            if (!this.plugin.canvas3d) return;
-            const renderer = this.plugin.canvas3d.props.renderer;
-            const color: typeof SimpleSettingsParams['background']['defaultValue'] = p.value;
-            if (color.name === 'transparent') {
-                PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { renderer: { ...renderer, backgroundColor: ColorNames.white }, transparentBackground: true } });
-            } else {
-                PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { renderer: { ...renderer, backgroundColor: color.params.color }, transparentBackground: false } });
-            }
-        } else if (p.name === 'renderStyle') {
-            if (!this.plugin.canvas3d) return;
-
-            const renderer = this.plugin.canvas3d.props.renderer;
-            if (p.value === 'flat') {
-                PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                    renderer: { ...renderer, lightIntensity: 0, ambientIntensity: 1, roughness: 0.4, metalness: 0 }
-                } });
-            } else if (p.value === 'matte') {
-                PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                    renderer: { ...renderer, lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 1, metalness: 0 }
-                } });
-            } else if (p.value === 'glossy') {
-                PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                    renderer: { ...renderer, lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 0.4, metalness: 0 }
-                } });
-            } else if (p.value === 'metallic') {
-                PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                    renderer: { ...renderer, lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 0.6, metalness: 0.4 }
-                } });
-            }
-        } else if (p.name === 'occlusion') {
-            if (!this.plugin.canvas3d) return;
-            const postprocessing = this.plugin.canvas3d.props.postprocessing;
-            PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                postprocessing: { ...postprocessing, occlusionEnable: p.value, occlusionBias: 0.5, occlusionRadius: 64 },
-            } });
-        } else if (p.name === 'outline') {
-            if (!this.plugin.canvas3d) return;
-            const postprocessing = this.plugin.canvas3d.props.postprocessing;
-            PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                postprocessing: { ...postprocessing, outlineEnable: p.value },
-            } });
-        } else if (p.name === 'fog') {;
-            PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                cameraFog: p.value ? 50 : 0,
-            } });
-        } else if (p.name === 'clipFar') {;
-            PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                cameraClipFar: p.value,
-            } });
-        }
-    }
-
-    get values () {
-        const renderer = this.plugin.canvas3d?.props.renderer;
-
-        let renderStyle = 'custom'
-        let background: typeof SimpleSettingsParams['background']['defaultValue'] = { name: 'transparent', params: { } }
+type SimpleSettingsParams = typeof SimpleSettingsParams
+const SimpleSettingsMapping = ParamMapping({
+    params: SimpleSettingsParams,
+    target(ctx: PluginContext) { return ctx.canvas3d?.props!; } })({
+    values(t, ctx) {
+        const renderer = t.renderer;
 
+        let renderStyle: SimpleSettingsParams['renderStyle']['defaultValue'] = 'custom' as any;
         if (renderer) {
             if (renderer.lightIntensity === 0 && renderer.ambientIntensity === 1 && renderer.roughness === 0.4 && renderer.metalness === 0) {
                 renderStyle = 'flat'
@@ -107,31 +61,42 @@ export class SimpleSettingsControl extends PluginUIComponent {
                     renderStyle = 'metallic'
                 }
             }
-
-            if (renderer.backgroundColor === ColorNames.white && this.plugin.canvas3d?.props.transparentBackground) {
-                background = { name: 'transparent', params: { } }
-            } else {
-                background = { name: 'opaque', params: { color: renderer.backgroundColor } }
-            }
         }
 
         return {
-            spin: !!this.plugin.canvas3d?.props.trackball.spin,
-            camera: this.plugin.canvas3d?.props.cameraMode,
-            background,
+            spin: !!t.trackball.spin,
+            camera: t.cameraMode,
+            background:  (renderer.backgroundColor === ColorNames.white && t.transparentBackground) 
+                ? { name: 'transparent', params: { } }
+                : { name: 'opaque', params: { color: renderer.backgroundColor } },
             renderStyle,
-            occlusion: this.plugin.canvas3d?.props.postprocessing.occlusionEnable,
-            outline: this.plugin.canvas3d?.props.postprocessing.outlineEnable,
-            fog: this.plugin.canvas3d ? this.plugin.canvas3d.props.cameraFog > 1 : false,
-            clipFar: this.plugin.canvas3d?.props.cameraClipFar
+            occlusion: t.postprocessing.occlusionEnable,
+            outline: t.postprocessing.outlineEnable,
+            fog: ctx.canvas3d ? t.cameraFog > 1 : false,
+            clipFar: t.cameraClipFar
+        };
+    },
+    update(s, t) {
+        t.trackball.spin = s.spin;
+        t.cameraMode = s.camera;
+        t.transparentBackground = s.background.name === 'transparent';
+        t.renderer.backgroundColor = s.background.name === 'transparent' ? ColorNames.white : s.background.params.color;
+        switch (s.renderStyle) {
+            case 'flat': Object.assign(t.renderer, { lightIntensity: 0, ambientIntensity: 1, roughness: 0.4, metalness: 0 }); break;
+            case 'matte':  Object.assign(t.renderer, { lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 1, metalness: 0 }); break;
+            case 'glossy':  Object.assign(t.renderer, { lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 0.4, metalness: 0 }); break;
+            case 'metallic':  Object.assign(t.renderer, { lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 0.6, metalness: 0.4 }); break;
         }
+        t.postprocessing.occlusionEnable = s.occlusion;
+        if (s.occlusion) { 
+            t.postprocessing.occlusionBias = 0.5;
+            t.postprocessing.occlusionRadius = 64;
+        }
+        t.postprocessing.outlineEnable = s.outline;
+        t.cameraFog = s.fog ? 50 : 0;
+        t.cameraClipFar = s.clipFar;
+    },
+    apply(settings, ctx) {
+        return PluginCommands.Canvas3D.SetSettings.dispatch(ctx, { settings });
     }
-
-    componentDidMount() {
-        this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
-    }
-
-    render() {
-        return <ParameterControls params={SimpleSettingsParams} values={this.values} onChange={this.setSettings} />
-    }
-}
\ No newline at end of file
+})
\ No newline at end of file
diff --git a/src/mol-plugin/behavior/dynamic/custom-props.ts b/src/mol-plugin/behavior/dynamic/custom-props.ts
index 6b5428cacbec1b4744308c6d5f88bdef7fb5b426..91586c5cfe9ac4294e0b5896f769604f5cee8e2e 100644
--- a/src/mol-plugin/behavior/dynamic/custom-props.ts
+++ b/src/mol-plugin/behavior/dynamic/custom-props.ts
@@ -10,6 +10,8 @@ export { Interactions } from './custom-props/computed/interactions'
 export { SecondaryStructure } from './custom-props/computed/secondary-structure'
 export { ValenceModel } from './custom-props/computed/valence-model'
 
+export { CrossLinkRestraint } from './custom-props/integrative/cross-link-restraint'
+
 export { PDBeStructureQualityReport } from './custom-props/pdbe/structure-quality-report'
 export { RCSBAssemblySymmetry } from './custom-props/rcsb/assembly-symmetry'
 export { RCSBValidationReport } from './custom-props/rcsb/validation-report'
\ No newline at end of file
diff --git a/src/mol-plugin/behavior/dynamic/custom-props/computed/accessible-surface-area.ts b/src/mol-plugin/behavior/dynamic/custom-props/computed/accessible-surface-area.ts
index aa95e36eb6e73a018fe22ebfbf13857eb78b0669..b5013fced3479ffdcc2b73838a4755ad3d472a5e 100644
--- a/src/mol-plugin/behavior/dynamic/custom-props/computed/accessible-surface-area.ts
+++ b/src/mol-plugin/behavior/dynamic/custom-props/computed/accessible-surface-area.ts
@@ -11,6 +11,7 @@ import { Loci } from '../../../../../mol-model/loci';
 import { AccessibleSurfaceAreaColorThemeProvider } from '../../../../../mol-model-props/computed/themes/accessible-surface-area';
 import { OrderedSet } from '../../../../../mol-data/int';
 import { arraySum } from '../../../../../mol-util/array';
+import { DefaultQueryRuntimeTable } from '../../../../../mol-script/runtime/query/compiler';
 
 export const AccessibleSurfaceArea = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({
     name: 'computed-accessible-surface-area-prop',
@@ -36,12 +37,17 @@ export const AccessibleSurfaceArea = PluginBehavior.create<{ autoAttach: boolean
         }
 
         register(): void {
+            DefaultQueryRuntimeTable.addCustomProp(this.provider.descriptor);
+
             this.ctx.customStructureProperties.register(this.provider, this.params.autoAttach);
             this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add('accessible-surface-area', AccessibleSurfaceAreaColorThemeProvider)
             this.ctx.lociLabels.addProvider(this.label);
         }
 
         unregister() {
+            // TODO
+            // DefaultQueryRuntimeTable.removeCustomProp(this.provider.descriptor);
+
             this.ctx.customStructureProperties.unregister(this.provider.descriptor.name);
             this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove('accessible-surface-area')
             this.ctx.lociLabels.removeProvider(this.label);
diff --git a/src/mol-plugin/behavior/dynamic/custom-props/integrative/cross-link-restraint.ts b/src/mol-plugin/behavior/dynamic/custom-props/integrative/cross-link-restraint.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a1a105666a5813a1cf7583e3f6247a886b7f5ec4
--- /dev/null
+++ b/src/mol-plugin/behavior/dynamic/custom-props/integrative/cross-link-restraint.ts
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { PluginBehavior } from '../../../behavior';
+import { ModelCrossLinkRestraint } from '../../../../../mol-model-props/integrative/cross-link-restraint/format';
+import { Model } from '../../../../../mol-model/structure';
+import { MmcifFormat } from '../../../../../mol-model-formats/structure/mmcif';
+import { CrossLinkRestraintRepresentationProvider } from '../../../../../mol-model-props/integrative/cross-link-restraint/representation';
+import { CrossLinkColorThemeProvider } from '../../../../../mol-model-props/integrative/cross-link-restraint/color';
+import { CrossLinkRestraint as _CrossLinkRestraint } from '../../../../../mol-model-props/integrative/cross-link-restraint/property';
+
+const Tag = _CrossLinkRestraint.Tag
+
+export const CrossLinkRestraint = PluginBehavior.create<{ }>({
+    name: 'integrative-cross-link-restraint',
+    category: 'custom-props',
+    display: { name: 'Cross Link Restraint' },
+    ctor: class extends PluginBehavior.Handler<{ }> {
+        private provider = ModelCrossLinkRestraint.Provider
+
+        register(): void {
+            this.provider.formatRegistry.add('mmCIF', crossLinkRestraintFromMmcif)
+
+            this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add(Tag.CrossLinkRestraint, CrossLinkColorThemeProvider)
+            this.ctx.structureRepresentation.registry.add(Tag.CrossLinkRestraint, CrossLinkRestraintRepresentationProvider)
+        }
+
+        unregister() {
+            this.provider.formatRegistry.remove('mmCIF')
+
+            this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove(Tag.CrossLinkRestraint)
+            this.ctx.structureRepresentation.registry.remove(Tag.CrossLinkRestraint)
+        }
+    }
+});
+
+function crossLinkRestraintFromMmcif(model: Model) {
+    if (!MmcifFormat.is(model.sourceData)) return;
+    const { ihm_cross_link_restraint } = model.sourceData.data.db;
+    if (ihm_cross_link_restraint._rowCount === 0) return;
+    return ModelCrossLinkRestraint.fromTable(ihm_cross_link_restraint, model)
+}
\ No newline at end of file
diff --git a/src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts b/src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts
index 69742b2992896625fd383526de1a7e8183c90520..ce22b7c29bed4f7e5215c66bec5bbfdbdc47476e 100644
--- a/src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts
+++ b/src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts
@@ -15,7 +15,10 @@ import { PluginBehavior } from '../../../behavior';
 export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({
     name: 'pdbe-structure-quality-report-prop',
     category: 'custom-props',
-    display: { name: 'PDBe Structure Quality Report' },
+    display: {
+        name: 'Structure Quality Report',
+        description: 'Data from wwPDB Validation Report, obtained via PDBe.'
+    },
     ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showTooltip: boolean }> {
 
         private provider = StructureQualityReportProvider
@@ -32,8 +35,8 @@ export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: bo
 
                     const se = StructureElement.Location.create(loci.structure, u, u.elements[OrderedSet.getAt(e.indices, 0)]);
                     const issues = StructureQualityReport.getIssues(se);
-                    if (issues.length === 0) return 'PDBe Validation: No Issues';
-                    return `PDBe Validation: ${issues.join(', ')}`;
+                    if (issues.length === 0) return 'Validation: No Issues';
+                    return `Validation: ${issues.join(', ')}`;
 
                 default: return void 0;
             }
diff --git a/src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts b/src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts
index 803f2b45daeed063f6e8d43ead4fde991f1f5df0..32aeba1bd6b85eec5bbd8d2580a64fc0686b1900 100644
--- a/src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts
+++ b/src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts
@@ -14,17 +14,22 @@ import { Task } from '../../../../../mol-task';
 import { PluginContext } from '../../../../context';
 import { StateTransformer, StateAction, StateObject } from '../../../../../mol-state';
 
+const Tag = AssemblySymmetry.Tag
+
 export const RCSBAssemblySymmetry = PluginBehavior.create<{ autoAttach: boolean }>({
     name: 'rcsb-assembly-symmetry-prop',
     category: 'custom-props',
-    display: { name: 'RCSB Assembly Symmetry' },
+    display: {
+        name: 'Assembly Symmetry',
+        description: 'Assembly Symmetry data calculated with BioJava, obtained via RCSB PDB.'
+    },
     ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
         private provider = AssemblySymmetryProvider
 
         register(): void {
             this.ctx.state.dataState.actions.add(InitAssemblySymmetry3D)
             this.ctx.customStructureProperties.register(this.provider, this.params.autoAttach);
-            this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add('rcsb-assembly-symmetry-cluster', AssemblySymmetryClusterColorThemeProvider)
+            this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add(Tag.Cluster, AssemblySymmetryClusterColorThemeProvider)
         }
 
         update(p: { autoAttach: boolean }) {
@@ -37,7 +42,7 @@ export const RCSBAssemblySymmetry = PluginBehavior.create<{ autoAttach: boolean
         unregister() {
             this.ctx.state.dataState.actions.remove(InitAssemblySymmetry3D)
             this.ctx.customStructureProperties.unregister(this.provider.descriptor.name);
-            this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove('rcsb-assembly-symmetry-cluster')
+            this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove(Tag.Cluster)
         }
     },
     params: () => ({
@@ -47,24 +52,32 @@ export const RCSBAssemblySymmetry = PluginBehavior.create<{ autoAttach: boolean
 });
 
 const InitAssemblySymmetry3D = StateAction.build({
-    display: { name: 'RCSB Assembly Symmetry' },
+    display: {
+        name: 'Assembly Symmetry',
+        description: 'Initialize Assembly Symmetry axes and cage. Data calculated with BioJava, obtained via RCSB PDB.'
+    },
     from: PluginStateObject.Molecule.Structure,
     isApplicable: (a) => AssemblySymmetry.isApplicable(a.data)
-})(({ a, ref, state }, plugin: PluginContext) => Task.create('Init RCSB Assembly Symmetry', async ctx => {
+})(({ a, ref, state }, plugin: PluginContext) => Task.create('Init Assembly Symmetry', async ctx => {
     try {
         await AssemblySymmetryProvider.attach({ runtime: ctx, fetch: plugin.fetch }, a.data)
     } catch(e) {
-        plugin.log.error(`RCSB Assembly Symmetry: ${e}`)
+        plugin.log.error(`Assembly Symmetry: ${e}`)
         return
     }
     const tree = state.build().to(ref).apply(AssemblySymmetry3D);
     await state.updateTree(tree).runInContext(ctx);
 }));
 
+export { AssemblySymmetry3D }
+
 type AssemblySymmetry3D = typeof AssemblySymmetry3D
 const AssemblySymmetry3D = PluginStateTransform.BuiltIn({
-    name: 'rcsb-assembly-symmetry-3d',
-    display: 'RCSB Assembly Symmetry',
+    name: Tag.Representation,
+    display: {
+        name: 'Assembly Symmetry',
+        description: 'Assembly Symmetry axes and cage. Data calculated with BioJava, obtained via RCSB PDB.'
+    },
     from: PluginStateObject.Molecule.Structure,
     to: PluginStateObject.Shape.Representation3D,
     params: (a) => {
@@ -78,7 +91,7 @@ const AssemblySymmetry3D = PluginStateTransform.BuiltIn({
         return true;
     },
     apply({ a, params }, plugin: PluginContext) {
-        return Task.create('RCSB Assembly Symmetry', async ctx => {
+        return Task.create('Assembly Symmetry', async ctx => {
             await AssemblySymmetryProvider.attach({ runtime: ctx, fetch: plugin.fetch }, a.data)
             const assemblySymmetry = AssemblySymmetryProvider.get(a.data).value
             if (!assemblySymmetry || assemblySymmetry.length === 0) {
@@ -91,7 +104,7 @@ const AssemblySymmetry3D = PluginStateTransform.BuiltIn({
         });
     },
     update({ a, b, newParams }, plugin: PluginContext) {
-        return Task.create('RCSB Assembly Symmetry', async ctx => {
+        return Task.create('Assembly Symmetry', async ctx => {
             await AssemblySymmetryProvider.attach({ runtime: ctx, fetch: plugin.fetch }, a.data)
             const assemblySymmetry = AssemblySymmetryProvider.get(a.data).value
             if (!assemblySymmetry || assemblySymmetry.length === 0) {
diff --git a/src/mol-plugin/behavior/dynamic/custom-props/rcsb/validation-report.ts b/src/mol-plugin/behavior/dynamic/custom-props/rcsb/validation-report.ts
index e8ec2fad939d038830e26bdad8da2d1c762361f7..94b90517cc7f6da20f045dcce6f745acb00e1afd 100644
--- a/src/mol-plugin/behavior/dynamic/custom-props/rcsb/validation-report.ts
+++ b/src/mol-plugin/behavior/dynamic/custom-props/rcsb/validation-report.ts
@@ -14,13 +14,17 @@ import { OrderedSet } from '../../../../../mol-data/int';
 import { ClashesRepresentationProvider } from '../../../../../mol-model-props/rcsb/representations/validation-report-clashes';
 import { DensityFitColorThemeProvider } from '../../../../../mol-model-props/rcsb/themes/density-fit';
 import { cantorPairing } from '../../../../../mol-data/util';
+import { DefaultQueryRuntimeTable } from '../../../../../mol-script/runtime/query/compiler';
 
 const Tag = ValidationReport.Tag
 
 export const RCSBValidationReport = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({
     name: 'rcsb-validation-report-prop',
     category: 'custom-props',
-    display: { name: 'RCSB Validation Report' },
+    display: {
+        name: 'Validation Report',
+        description: 'Data from wwPDB Validation Report, obtained via RCSB PDB.'
+    },
     ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showTooltip: boolean }> {
         private provider = ValidationReportProvider
 
@@ -34,6 +38,8 @@ export const RCSBValidationReport = PluginBehavior.create<{ autoAttach: boolean,
         }
 
         register(): void {
+            DefaultQueryRuntimeTable.addCustomProp(this.provider.descriptor);
+
             this.ctx.customModelProperties.register(this.provider, this.params.autoAttach);
 
             this.ctx.lociLabels.addProvider(this.label);
@@ -54,6 +60,9 @@ export const RCSBValidationReport = PluginBehavior.create<{ autoAttach: boolean,
         }
 
         unregister() {
+            // TODO
+            // DefaultQueryRuntimeTable.removeCustomProp(this.provider.descriptor);
+
             this.ctx.customStructureProperties.unregister(this.provider.descriptor.name);
 
             this.ctx.lociLabels.removeProvider(this.label);
@@ -93,7 +102,7 @@ function geometryQualityLabel(loci: Loci): string | undefined {
             if (angles) angles.forEach(a => issues.add(angleOutliers.data[a].tag))
 
             if (issues.size === 0) {
-                return `RCSB Geometry Quality <small>(1 Atom)</small>: no issues`;
+                return `Geometry Quality <small>(1 Atom)</small>: no issues`;
             }
 
             const summary: string[] = []
diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts
index ed27bf3cadff50819fe399260972f5498a4b0b9d..85e873d15309e9d490c547ee8fbbff889497431e 100644
--- a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts
+++ b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts
@@ -93,8 +93,8 @@ export namespace VolumeStreaming {
                 }, { description: 'Static box defined by cartesian coords.', isFlat: true }),
                 'selection-box': PD.Group({
                     radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }, { description: 'Radius in \u212B within which the volume is shown.' }),
-                    bottomLeft: PD.Vec3(Vec3.create(0, 0, 0), { isHidden: true }),
-                    topRight: PD.Vec3(Vec3.create(0, 0, 0), { isHidden: true }),
+                    bottomLeft: PD.Vec3(Vec3.create(0, 0, 0), {}, { isHidden: true }),
+                    topRight: PD.Vec3(Vec3.create(0, 0, 0), {}, { isHidden: true }),
                 }, { description: 'Box around last-interacted element.', isFlat: true }),
                 'cell': PD.Group({}),
                 // 'auto': PD.Group({  }), // TODO based on camera distance/active selection/whatever, show whole structure or slice.
diff --git a/src/mol-plugin/behavior/static/camera.ts b/src/mol-plugin/behavior/static/camera.ts
index 32971d8edc6ae922e406d40e21293f20c74290a0..dbe81f4af08aa43ab72f2f00ed78f390744aa2c7 100644
--- a/src/mol-plugin/behavior/static/camera.ts
+++ b/src/mol-plugin/behavior/static/camera.ts
@@ -15,8 +15,8 @@ export function registerDefault(ctx: PluginContext) {
 }
 
 export function Reset(ctx: PluginContext) {
-    PluginCommands.Camera.Reset.subscribe(ctx, () => {
-        ctx.canvas3d?.requestCameraReset();
+    PluginCommands.Camera.Reset.subscribe(ctx, options => {
+        ctx.canvas3d?.requestCameraReset(options);
     })
 }
 
diff --git a/src/mol-plugin/command.ts b/src/mol-plugin/command.ts
index a861e3ba3f4b70527e3c06f5a7bda3268467bf2b..e4e8e8532dccb0eeb8e2085f230563d3830072df 100644
--- a/src/mol-plugin/command.ts
+++ b/src/mol-plugin/command.ts
@@ -59,7 +59,7 @@ export const PluginCommands = {
         Hide: PluginCommand<{ key: string }>()
     },
     Camera: {
-        Reset: PluginCommand<{}>(),
+        Reset: PluginCommand<{ durationMs?: number, snapshot?: Partial<Camera.Snapshot> }>(),
         SetSnapshot: PluginCommand<{ snapshot: Partial<Camera.Snapshot>, durationMs?: number }>(),
         Snapshots: {
             Add: PluginCommand<{ name?: string, description?: string }>(),
diff --git a/src/mol-plugin/index.ts b/src/mol-plugin/index.ts
index 9c43953cd18827be185aeac38a5988635e214bc9..16fcadc04e4c3a240ebadd97ee9f6c7e2377d3ab 100644
--- a/src/mol-plugin/index.ts
+++ b/src/mol-plugin/index.ts
@@ -70,14 +70,17 @@ export const DefaultPluginSpec: PluginSpec = {
         PluginSpec.Behavior(PluginBehaviors.Representation.SelectLoci),
         PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider),
         PluginSpec.Behavior(PluginBehaviors.Camera.FocusLoci),
+        PluginSpec.Behavior(StructureRepresentationInteraction),
+
         PluginSpec.Behavior(PluginBehaviors.CustomProps.AccessibleSurfaceArea),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.Interactions),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.SecondaryStructure),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.ValenceModel),
+        PluginSpec.Behavior(PluginBehaviors.CustomProps.CrossLinkRestraint),
+
         PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: true, showTooltip: true }),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.RCSBAssemblySymmetry),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.RCSBValidationReport),
-        PluginSpec.Behavior(StructureRepresentationInteraction)
     ],
     customParamEditors: [
         [CreateVolumeStreamingBehavior, VolumeStreamingCustomControls]
diff --git a/src/mol-plugin/state/representation/model.ts b/src/mol-plugin/state/representation/model.ts
index 47abb3649f33ac19334f3edca6fddc52675692c9..fee6cbda0b3429fbb55548eb5b03127da3f9b3e0 100644
--- a/src/mol-plugin/state/representation/model.ts
+++ b/src/mol-plugin/state/representation/model.ts
@@ -1,12 +1,13 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 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 { Model, Structure, StructureSymmetry } from '../../../mol-model/structure';
 import { stringToWords } from '../../../mol-util/string';
-import { SpacegroupCell } from '../../../mol-math/geometry';
+import { SpacegroupCell, Spacegroup } from '../../../mol-math/geometry';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { Vec3 } from '../../../mol-math/linear-algebra';
 import { RuntimeContext } from '../../../mol-task';
@@ -16,12 +17,28 @@ import { PluginStateObject as SO } from '../objects';
 import { ModelSymmetry } from '../../../mol-model-formats/structure/property/symmetry';
 
 export namespace ModelStructureRepresentation {
-    export function getParams(model?: Model, defaultValue?: 'deposited' | 'assembly' | 'symmetry' | 'symmetry-mates') {
+    export function getParams(model?: Model, defaultValue?: 'deposited' | 'assembly' | 'symmetry' | 'symmetry-mates' | 'symmetry-assembly') {
         const symmetry = model && ModelSymmetry.Provider.get(model)
 
         const assemblyIds = symmetry ? symmetry.assemblies.map(a => [a.id, `${a.id}: ${stringToWords(a.details)}`] as [string, string]) : [];
         const showSymm = !symmetry ? true : !SpacegroupCell.isZero(symmetry.spacegroup.cell);
 
+        const operatorOptions: [number, string][] = []
+        if (symmetry) {
+            const { operators } = symmetry.spacegroup
+            for (let i = 0, il = operators.length; i < il; i++) {
+                operatorOptions.push([i, `${i + 1}: ${Spacegroup.getOperatorXyz(operators[i])}`])
+            }
+        }
+
+        const asymIdsOptions: [string, string][] = []
+        if (model) {
+            model.properties.structAsymMap.forEach(v => {
+                const label = v.id === v.auth_id ? v.id : `${v.id} [auth ${v.auth_id}]`
+                asymIdsOptions.push([v.id, label])
+            })
+        }
+
         const modes = {
             deposited: PD.EmptyGroup(),
             assembly: PD.Group({
@@ -33,8 +50,21 @@ export namespace ModelStructureRepresentation {
                 radius: PD.Numeric(5)
             }, { isFlat: true }),
             'symmetry': PD.Group({
-                ijkMin: PD.Vec3(Vec3.create(-1, -1, -1), { label: 'Min IJK', fieldLabels: { x: 'I', y: 'J', z: 'K' } }),
-                ijkMax: PD.Vec3(Vec3.create(1, 1, 1), { label: 'Max IJK', fieldLabels: { x: 'I', y: 'J', z: 'K' } })
+                ijkMin: PD.Vec3(Vec3.create(-1, -1, -1), { step: 1 }, { label: 'Min IJK', fieldLabels: { x: 'I', y: 'J', z: 'K' } }),
+                ijkMax: PD.Vec3(Vec3.create(1, 1, 1), { step: 1 }, { label: 'Max IJK', fieldLabels: { x: 'I', y: 'J', z: 'K' } })
+            }, { isFlat: true }),
+            'symmetry-assembly': PD.Group({
+                generators: PD.ObjectList({
+                    operators: PD.ObjectList({
+                        index: PD.Select(0, operatorOptions),
+                        shift: PD.Vec3(Vec3(), { step: 1 }, { label: 'IJK', fieldLabels: { x: 'I', y: 'J', z: 'K' } })
+                    }, e => `${e.index + 1}_${e.shift.map(a => a + 5).join('')}`, {
+                        defaultValue: [] as { index: number, shift: Vec3 }[]
+                    }),
+                    asymIds: PD.MultiSelect([] as string[], asymIdsOptions)
+                }, e => `${e.asymIds.length} asym ids, ${e.operators.length} operators`, {
+                    defaultValue: [] as { operators: { index: number, shift: Vec3 }[], asymIds: string[] }[]
+                })
             }, { isFlat: true })
         };
 
@@ -49,6 +79,7 @@ export namespace ModelStructureRepresentation {
         if (showSymm) {
             options.push(['symmetry-mates', 'Symmetry Mates']);
             options.push(['symmetry', 'Symmetry (indices)']);
+            options.push(['symmetry-assembly', 'Symmetry (assembly)']);
         }
 
         return {
@@ -105,8 +136,16 @@ export namespace ModelStructureRepresentation {
         return new SO.Molecule.Structure(s, props);
     }
 
+    async function buildSymmetryAssembly(ctx: RuntimeContext, model: Model, generators: StructureSymmetry.Generators, symmetry: Symmetry) {
+        const base = Structure.ofModel(model);
+        const s = await StructureSymmetry.buildSymmetryAssembly(base, generators, symmetry).runInContext(ctx);
+        const props = { label: `Symmetry Assembly`, description: Structure.elementDescription(s) };
+        return new SO.Molecule.Structure(s, props);
+    }
+
     export async function create(plugin: PluginContext, ctx: RuntimeContext, model: Model, params?: Params): Promise<SO.Molecule.Structure> {
-        if (!params || params.name === 'deposited') {
+        const symmetry = ModelSymmetry.Provider.get(model)
+        if (!symmetry || !params || params.name === 'deposited') {
             const s = Structure.ofModel(model);
             return new SO.Molecule.Structure(s, { label: 'Deposited', description: Structure.elementDescription(s) });
         }
@@ -119,6 +158,9 @@ export namespace ModelStructureRepresentation {
         if (params.name === 'symmetry-mates') {
             return buildSymmetryMates(ctx, model, params.params.radius)
         }
+        if (params.name === 'symmetry-assembly') {
+            return buildSymmetryAssembly(ctx, model, params.params.generators, symmetry)
+        }
 
         throw new Error(`Unknown represetation type: ${(params as any).name}`);
     }
diff --git a/src/mol-plugin/state/representation/structure/preset.ts b/src/mol-plugin/state/representation/structure/preset.ts
index 418cbb6405616bf58e8c5bb0770f51922380e938..f9bebdd26fc8923bed8ce14d8a929d7f9ee4240b 100644
--- a/src/mol-plugin/state/representation/structure/preset.ts
+++ b/src/mol-plugin/state/representation/structure/preset.ts
@@ -52,7 +52,7 @@ const defaultPreset = StructureRepresentationProvider({
         const ligandRepr = ligand.applyOrUpdateTagged(reprTags, StateTransforms.Representation.StructureRepresentation3D,
             StructureRepresentation3DHelpers.getDefaultParams(plugin, 'ball-and-stick', structure));
 
-        applyComplex(root, 'modified')
+        applyComplex(root, 'non-standard')
             .applyOrUpdateTagged(reprTags, StateTransforms.Representation.StructureRepresentation3D,
                 StructureRepresentation3DHelpers.getDefaultParamsWithTheme(plugin, 'ball-and-stick', 'polymer-id', structure, void 0));
 
diff --git a/src/mol-plugin/state/transforms/model.ts b/src/mol-plugin/state/transforms/model.ts
index d593a97b01f8ae8f2aa79c5d5577ee468e0eee94..bb25ec0f9a602b8581483ce21003e4c117dbe993 100644
--- a/src/mol-plugin/state/transforms/model.ts
+++ b/src/mol-plugin/state/transforms/model.ts
@@ -16,7 +16,6 @@ import Expression from '../../../mol-script/language/expression';
 import { StateObject, StateTransformer } from '../../../mol-state';
 import { RuntimeContext, Task } from '../../../mol-task';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
-import { stringToWords } from '../../../mol-util/string';
 import { PluginStateObject as SO, PluginStateTransform } from '../objects';
 import { trajectoryFromGRO } from '../../../mol-model-formats/structure/gro';
 import { parseGRO } from '../../../mol-io/reader/gro/parser';
@@ -32,7 +31,6 @@ import { parseDcd } from '../../../mol-io/reader/dcd/parser';
 import { coordinatesFromDcd } from '../../../mol-model-formats/structure/dcd';
 import { topologyFromPsf } from '../../../mol-model-formats/structure/psf';
 import { deepEqual } from '../../../mol-util';
-import { ModelSymmetry } from '../../../mol-model-formats/structure/property/symmetry';
 
 export { CoordinatesFromDcd };
 export { TopologyFromPsf };
@@ -45,7 +43,6 @@ export { TrajectoryFrom3DG };
 export { ModelFromTrajectory };
 export { StructureFromTrajectory };
 export { StructureFromModel };
-export { StructureAssemblyFromModel };
 export { TransformStructureConformation };
 export { TransformStructureConformationByMatrix };
 export { StructureSelectionFromExpression };
@@ -290,32 +287,6 @@ const StructureFromModel = PluginStateTransform.BuiltIn({
     }
 });
 
-// TODO: deprecate this in favor of StructureFromModel
-type StructureAssemblyFromModel = typeof StructureAssemblyFromModel
-const StructureAssemblyFromModel = PluginStateTransform.BuiltIn({
-    name: 'structure-assembly-from-model',
-    display: { name: 'Structure Assembly', description: 'Create a molecular structure assembly.' },
-    from: SO.Molecule.Model,
-    to: SO.Molecule.Structure,
-    params(a) {
-        if (!a) {
-            return { id: PD.Optional(PD.Text('', { label: 'Assembly Id', description: 'Assembly Id. Value \'deposited\' can be used to specify deposited asymmetric unit.' })) };
-        }
-        const assemblies = ModelSymmetry.Provider.get(a.data)?.assemblies || []
-        const ids = assemblies.map(a => [a.id, `${a.id}: ${stringToWords(a.details)}`] as [string, string]);
-        ids.push(['deposited', 'Deposited']);
-        return {
-            id: PD.Optional(PD.Select(ids[0][0], ids, { label: 'Asm Id', description: 'Assembly Id' }))
-        };
-    }
-})({
-    apply({ a, params }, plugin: PluginContext) {
-        return Task.create('Build Assembly', async ctx => {
-            return ModelStructureRepresentation.create(plugin, ctx, a.data, { name: 'assembly', params });
-        })
-    }
-});
-
 const _translation = Vec3(), _m = Mat4(), _n = Mat4();
 type TransformStructureConformation = typeof TransformStructureConformation
 const TransformStructureConformation = PluginStateTransform.BuiltIn({
@@ -643,7 +614,7 @@ export const StructureComplexElementTypes = {
 
     'branched': 'branched', // = carbs
     'ligand': 'ligand',
-    'modified': 'modified',
+    'non-standard': 'non-standard',
 
     'coarse': 'coarse',
 
@@ -678,7 +649,7 @@ const StructureComplexElement = PluginStateTransform.BuiltIn({
             case 'branched': query = StructureSelectionQueries.branchedPlusConnected.query; label = 'Branched'; break;
             case 'ligand': query = StructureSelectionQueries.ligandPlusConnected.query; label = 'Ligand'; break;
 
-            case 'modified': query = StructureSelectionQueries.modified.query; label = 'Modified'; break;
+            case 'non-standard': query = StructureSelectionQueries.nonStandardPolymer.query; label = 'Non-standard'; break;
 
             case 'coarse': query = StructureSelectionQueries.coarse.query; label = 'Coarse'; break;
 
@@ -719,7 +690,7 @@ async function attachModelProps(model: Model, ctx: PluginContext, taskCtx: Runti
     const { autoAttach, properties } = params
     for (const name of Object.keys(properties)) {
         const property = ctx.customModelProperties.get(name)
-        const props = params[name as keyof typeof params]
+        const props = properties[name]
         if (autoAttach.includes(name)) {
             try {
                 await property.attach(propertyCtx, model, props)
@@ -754,7 +725,7 @@ async function attachStructureProps(structure: Structure, ctx: PluginContext, ta
     const { autoAttach, properties } = params
     for (const name of Object.keys(properties)) {
         const property = ctx.customStructureProperties.get(name)
-        const props = params[name as keyof typeof params]
+        const props = properties[name]
         if (autoAttach.includes(name)) {
             try {
                 await property.attach(propertyCtx, structure, props)
diff --git a/src/mol-plugin/state/transforms/representation.ts b/src/mol-plugin/state/transforms/representation.ts
index 611d19c55790844b5bd6f68bb60c8121f857f134..263060eaa25003721fdcc19f05d5eb62270cc4a2 100644
--- a/src/mol-plugin/state/transforms/representation.ts
+++ b/src/mol-plugin/state/transforms/representation.ts
@@ -63,37 +63,40 @@ namespace StructureRepresentation3DHelpers {
         })
     }
 
+    export type Props<R extends RepresentationProvider<Structure, any, any> = any, C extends ColorTheme.Provider<any> = any, S extends SizeTheme.Provider<any> = any> = {
+        repr?: R | [R, (r: R, ctx: ThemeRegistryContext, s: Structure) => Partial<RepresentationProvider.ParamValues<R>>],
+        color?: C | [C, (c: C, ctx: ThemeRegistryContext) => Partial<ColorTheme.ParamValues<C>>],
+        size?: S | [S, (c: S, ctx: ThemeRegistryContext) => Partial<SizeTheme.ParamValues<S>>]
+    }
+
     export function createParams<R extends RepresentationProvider<Structure, any, any>, C extends ColorTheme.Provider<any>, S extends SizeTheme.Provider<any>>(
-        ctx: PluginContext, structure: Structure, params: {
-            repr?: R | [R, (r: R, ctx: ThemeRegistryContext, s: Structure) => Partial<RepresentationProvider.ParamValues<R>>],
-            color?: C | [C, (c: C, ctx: ThemeRegistryContext) => Partial<ColorTheme.ParamValues<C>>],
-            size?: S | [S, (c: S, ctx: ThemeRegistryContext) => Partial<SizeTheme.ParamValues<S>>]
-        }): StateTransformer.Params<StructureRepresentation3D> {
+        ctx: PluginContext, structure: Structure, props: Props<R, C, S> = {}): StateTransformer.Params<StructureRepresentation3D> {
 
-        const themeCtx = ctx.structureRepresentation.themeCtx
+        const { themeCtx } = ctx.structureRepresentation
+        const themeDataCtx = { structure }
 
-        const repr = params.repr
-            ? params.repr instanceof Array ? params.repr[0] : params.repr
+        const repr = props.repr
+            ? props.repr instanceof Array ? props.repr[0] : props.repr
             : ctx.structureRepresentation.registry.default.provider;
         const reprDefaultParams = PD.getDefaultValues(repr.getParams(themeCtx, structure));
-        const reprParams = params.repr instanceof Array
-            ? { ...reprDefaultParams, ...params.repr[1](repr as R, themeCtx, structure) }
+        const reprParams = props.repr instanceof Array
+            ? { ...reprDefaultParams, ...props.repr[1](repr as R, themeCtx, structure) }
             : reprDefaultParams;
 
-        const color = params.color
-            ? params.color instanceof Array ? params.color[0] : params.color
+        const color = props.color
+            ? props.color instanceof Array ? props.color[0] : props.color
             : themeCtx.colorThemeRegistry.get(repr.defaultColorTheme.name);
-        const colorDefaultParams = { ...PD.getDefaultValues(color.getParams(themeCtx)), ...repr.defaultColorTheme.props }
-        const colorParams = params.color instanceof Array
-            ? { ...colorDefaultParams, ...params.color[1](color as C, themeCtx) }
+        const colorDefaultParams = { ...PD.getDefaultValues(color.getParams(themeDataCtx)), ...repr.defaultColorTheme.props }
+        const colorParams = props.color instanceof Array
+            ? { ...colorDefaultParams, ...props.color[1](color as C, themeCtx) }
             : colorDefaultParams;
 
-        const size = params.size
-            ? params.size instanceof Array ? params.size[0] : params.size
+        const size = props.size
+            ? props.size instanceof Array ? props.size[0] : props.size
             : themeCtx.sizeThemeRegistry.get(repr.defaultSizeTheme.name);
-        const sizeDefaultParams = { ...PD.getDefaultValues(size.getParams(themeCtx)), ...repr.defaultSizeTheme.props }
-        const sizeParams = params.size instanceof Array
-            ? { ...sizeDefaultParams, ...params.size[1](size as S, themeCtx) }
+        const sizeDefaultParams = { ...PD.getDefaultValues(size.getParams(themeDataCtx)), ...repr.defaultSizeTheme.props }
+        const sizeParams = props.size instanceof Array
+            ? { ...sizeDefaultParams, ...props.size[1](size as S, themeCtx) }
             : sizeDefaultParams;
 
         return ({
diff --git a/src/mol-plugin/util/structure-complex-helper.ts b/src/mol-plugin/util/structure-complex-helper.ts
index 59a428cc8f037ba995e6dfad62ef1d1384c9558e..af783510d5b2d0fe6786adf52226b8e17497b97e 100644
--- a/src/mol-plugin/util/structure-complex-helper.ts
+++ b/src/mol-plugin/util/structure-complex-helper.ts
@@ -22,7 +22,7 @@ export function createDefaultStructureComplex(
         .apply(StateTransforms.Representation.StructureRepresentation3D,
             StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick'));
 
-    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'modified' }, { tags: StructureComplexElementTypes.modified })
+    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'non-standard' }, { tags: StructureComplexElementTypes['non-standard'] })
         .apply(StateTransforms.Representation.StructureRepresentation3D,
             StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick', void 0, 'polymer-id'));
 
diff --git a/src/mol-plugin/util/structure-overpaint-helper.ts b/src/mol-plugin/util/structure-overpaint-helper.ts
index 05d7ea690925ad9ce6b893b9be46daafeb38c0a7..5a2b2da67e59e51c4b2e3c93618aa93c0cbda5ad 100644
--- a/src/mol-plugin/util/structure-overpaint-helper.ts
+++ b/src/mol-plugin/util/structure-overpaint-helper.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -7,10 +7,12 @@
 import { PluginStateObject } from '../../mol-plugin/state/objects';
 import { StateTransforms } from '../../mol-plugin/state/transforms';
 import { StateSelection, StateObjectCell, StateTransform, StateBuilder } from '../../mol-state';
-import { Structure, StructureElement } from '../../mol-model/structure';
+import { Structure, StructureElement, StructureSelection, QueryContext } from '../../mol-model/structure';
 import { PluginContext } from '../context';
 import { Color } from '../../mol-util/color';
 import { Overpaint } from '../../mol-theme/overpaint';
+import Expression from '../../mol-script/language/expression';
+import { compile } from '../../mol-script/runtime/query/compiler';
 
 type OverpaintEachReprCallback = (update: StateBuilder.Root, repr: StateObjectCell<PluginStateObject.Molecule.Structure.Representation3D, StateTransform<typeof StateTransforms.Representation.StructureRepresentation3D>>, overpaint?: StateObjectCell<any, StateTransform<typeof StateTransforms.Representation.OverpaintStructureRepresentation3DFromBundle>>) => void
 const OverpaintManagerTag = 'overpaint-controls'
@@ -29,7 +31,7 @@ export class StructureOverpaintHelper {
         await this.plugin.runTask(state.updateTree(update, { doNotUpdateCurrent: true }));
     }
 
-    async set(color: Color | -1, lociGetter: (structure: Structure) => StructureElement.Loci, types?: string[]) {
+    async set(color: Color | -1, lociGetter: (structure: Structure) => StructureElement.Loci, types?: string[], alpha = 1) {
         await this.eachRepr((update, repr, overpaintCell) => {
             if (types && !types.includes(repr.params!.values.type.name)) return
 
@@ -48,15 +50,23 @@ export class StructureOverpaintHelper {
             if (overpaintCell) {
                 const bundleLayers = [ ...overpaintCell.params!.values.layers, layer ]
                 const filtered = getFilteredBundle(bundleLayers, structure)
-                update.to(overpaintCell).update(Overpaint.toBundle(filtered, 1))
+                update.to(overpaintCell).update(Overpaint.toBundle(filtered, alpha))
             } else {
                 const filtered = getFilteredBundle([ layer ], structure)
                 update.to(repr.transform.ref)
-                    .apply(StateTransforms.Representation.OverpaintStructureRepresentation3DFromBundle, Overpaint.toBundle(filtered, 1), { tags: OverpaintManagerTag });
+                    .apply(StateTransforms.Representation.OverpaintStructureRepresentation3DFromBundle, Overpaint.toBundle(filtered, alpha), { tags: OverpaintManagerTag });
             }
         })
     }
 
+    async setFromExpression(color: Color | -1, expression: Expression, types?: string[], alpha = 1) {
+        return this.set(color, (structure) => {
+            const compiled = compile<StructureSelection>(expression)
+            const result = compiled(new QueryContext(structure))
+            return StructureSelection.toLociWithSourceUnits(result)
+        }, types, alpha)
+    }
+
     constructor(private plugin: PluginContext) {
 
     }
diff --git a/src/mol-plugin/util/structure-representation-helper.ts b/src/mol-plugin/util/structure-representation-helper.ts
index 6e58d3cae071cc81f2b5f1670cbd63048d9a2380..241d632bd9c4b8437f77e894de6b57c6f3c7770e 100644
--- a/src/mol-plugin/util/structure-representation-helper.ts
+++ b/src/mol-plugin/util/structure-representation-helper.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -12,8 +12,6 @@ import { PluginContext } from '../context';
 import { StructureRepresentation3DHelpers } from '../state/transforms/representation';
 import Expression from '../../mol-script/language/expression';
 import { compile } from '../../mol-script/runtime/query/compiler';
-import { StructureSelectionQueries as Q } from '../util/structure-selection-helper';
-import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
 import { VisualQuality } from '../../mol-geo/geometry/base';
 
 type StructureTransform = StateObjectCell<PSO.Molecule.Structure, StateTransform<StateTransformer<any, PSO.Molecule.Structure, any>>>
@@ -34,6 +32,12 @@ function getCombinedLoci(mode: SelectionModifier, loci: StructureElement.Loci, c
 
 type SelectionModifier = 'add' | 'remove' | 'only'
 
+type ReprProps = {
+    repr?: {},
+    color?: string | [string, {}],
+    size?: string | [string, {}],
+}
+
 export class StructureRepresentationHelper {
     getRepresentationStructure(rootRef: string, type: string) {
         const state = this.plugin.state.dataState
@@ -49,12 +53,52 @@ export class StructureRepresentationHelper {
         return selections.length > 0 ? selections[0] : undefined
     }
 
-    private async _set(modifier: SelectionModifier, type: string, loci: StructureElement.Loci, structure: StructureTransform, props = {}) {
+    private getRepresentationParams(structure: Structure, type: string, repr: RepresentationTransform | undefined, props: ReprProps = {}) {
+        const reprProps = {
+            ...(repr?.params && repr.params.values.type.params),
+            ignoreHydrogens: this._ignoreHydrogens,
+            quality: this._quality,
+            ...props.repr
+        }
+        const { themeCtx } =  this.plugin.structureRepresentation
+
+        const p: StructureRepresentation3DHelpers.Props = {
+            repr: [
+                this.plugin.structureRepresentation.registry.get(type),
+                () => reprProps
+            ]
+        }
+        if (props.color) {
+            const colorType = props.color instanceof Array ? props.color[0] : props.color
+            const colorTheme = themeCtx.colorThemeRegistry.get(colorType)
+            const colorProps = {
+                ...(repr?.params && repr.params.values.colorTheme.name === colorType && repr.params.values.colorTheme.params),
+                ...(props.color instanceof Array ? props.color[1] : {})
+            }
+            p.color = [colorTheme, () => colorProps]
+        }
+        if (props.size) {
+            const sizeType = props.size instanceof Array ? props.size[0] : props.size
+            const sizeTheme = themeCtx.sizeThemeRegistry.get(sizeType)
+            const sizeProps = {
+                ...(repr?.params && repr.params.values.sizeTheme.name === sizeType && repr.params.values.sizeTheme.params),
+                ...(props.size instanceof Array ? props.size[1] : {})
+            }
+            p.size = [sizeTheme, () => sizeProps]
+        }
+        if (props.size) p.size = props.size
+
+        return StructureRepresentation3DHelpers.createParams(this.plugin, structure, p)
+    }
+
+    private async _set(modifier: SelectionModifier, type: string, loci: StructureElement.Loci, structure: StructureTransform, props: ReprProps = {}) {
         const state = this.plugin.state.dataState
         const update = state.build()
         const s = structure.obj!.data
 
+        const repr = this.getRepresentation(structure.transform.ref, type)
         const reprStructure = this.getRepresentationStructure(structure.transform.ref, type)
+        const reprParams = this.getRepresentationParams(s.root, type, repr, props)
 
         if (reprStructure) {
             const currentLoci = StructureElement.Bundle.toLoci(reprStructure.params!.values.bundle, s)
@@ -64,14 +108,9 @@ export class StructureRepresentationHelper {
                 ...reprStructure.params!.values,
                 bundle: StructureElement.Bundle.fromLoci(combinedLoci)
             })
+            if (repr) update.to(repr).update(reprParams)
         } else {
             const combinedLoci = getCombinedLoci(modifier, loci, StructureElement.Loci.none(s))
-            const params = StructureRepresentation3DHelpers.getDefaultParams(this.plugin, type as any, s)
-
-            const p = params.type.params
-            Object.assign(p, props)
-            if (p.ignoreHydrogens !== undefined) p.ignoreHydrogens = this._ignoreHydrogens
-            if (p.quality !== undefined) p.quality = this._quality
 
             update.to(structure.transform.ref)
                 .apply(
@@ -79,13 +118,13 @@ export class StructureRepresentationHelper {
                     { bundle: StructureElement.Bundle.fromLoci(combinedLoci), label: type },
                     { tags: [ RepresentationManagerTag, getRepresentationManagerTag(type) ] }
                 )
-                .apply( StateTransforms.Representation.StructureRepresentation3D, params)
+                .apply(StateTransforms.Representation.StructureRepresentation3D, reprParams)
         }
 
         await this.plugin.runTask(state.updateTree(update, { doNotUpdateCurrent: true }))
     }
 
-    async set(modifier: SelectionModifier, type: string, lociGetter: (structure: Structure) => StructureElement.Loci, props = {}) {
+    async set(modifier: SelectionModifier, type: string, lociGetter: (structure: Structure) => StructureElement.Loci, props: ReprProps = {}) {
         const state = this.plugin.state.dataState;
         const structures = state.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Structure))
 
@@ -96,7 +135,7 @@ export class StructureRepresentationHelper {
         }
     }
 
-    async setFromExpression(modifier: SelectionModifier, type: string, expression: Expression, props = {}) {
+    async setFromExpression(modifier: SelectionModifier, type: string, expression: Expression, props: ReprProps = {}) {
         return this.set(modifier, type, (structure) => {
             const compiled = compile<StructureSelection>(expression)
             const result = compiled(new QueryContext(structure))
@@ -104,44 +143,76 @@ export class StructureRepresentationHelper {
         }, props)
     }
 
-    async clear() {
+    async eachStructure(callback: (structure: StructureTransform, type: string, update: StateBuilder.Root) => void) {
         const { registry } = this.plugin.structureRepresentation
         const state = this.plugin.state.dataState;
         const update = state.build()
         const structures = state.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Structure))
-        const bundle = StructureElement.Bundle.Empty
 
         for (const structure of structures) {
             for (let i = 0, il = registry.types.length; i < il; ++i) {
                 const type = registry.types[i][0]
                 const reprStructure = this.getRepresentationStructure(structure.transform.ref, type)
-                if (reprStructure) {
-                    update.to(reprStructure).update({ ...reprStructure.params!.values, bundle })
-                }
+                if (reprStructure) callback(reprStructure, type, update)
             }
         }
         await this.plugin.runTask(state.updateTree(update, { doNotUpdateCurrent: true }))
     }
 
-    async eachRepresentation(callback: (repr: RepresentationTransform, update: StateBuilder.Root) => void) {
+    async clear() {
+        const bundle = StructureElement.Bundle.Empty
+        await this.eachStructure((structure, type, update) => {
+            update.to(structure).update({ ...structure.params!.values, bundle })
+        })
+    }
+
+    async clearExcept(exceptTypes: string[]) {
+        const bundle = StructureElement.Bundle.Empty
+        await this.eachStructure((structure, type, update) => {
+            if (!exceptTypes.includes(type)) {
+                update.to(structure).update({ ...structure.params!.values, bundle })
+            }
+        })
+    }
+
+    async eachRepresentation(callback: (repr: RepresentationTransform, type: string, update: StateBuilder.Root) => void) {
         const { registry } = this.plugin.structureRepresentation
         const state = this.plugin.state.dataState;
         const update = state.build()
         const structures = state.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Structure))
         for (const structure of structures) {
             for (let i = 0, il = registry.types.length; i < il; ++i) {
-                const repr = this.getRepresentation(structure.transform.ref, registry.types[i][0])
-                if (repr) callback(repr, update)
+                const type = registry.types[i][0]
+                const repr = this.getRepresentation(structure.transform.ref, type)
+                if (repr) callback(repr, type, update)
             }
         }
         await this.plugin.runTask(state.updateTree(update, { doNotUpdateCurrent: true }))
     }
 
+    setRepresentationParams(repr: RepresentationTransform, type: string, update: StateBuilder.Root, props: ReprProps) {
+        const state = this.plugin.state.dataState;
+        const structures = state.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Structure))
+
+        for (const structure of structures) {
+            const s = structure.obj!.data
+            const reprParams = this.getRepresentationParams(s.root, type, repr, props)
+            update.to(repr).update(reprParams)
+        }
+    }
+
+    async updateRepresentation(repr: RepresentationTransform, type: string, props: ReprProps) {
+        const state = this.plugin.state.dataState;
+        const update = state.build()
+        this.setRepresentationParams(repr, type, update, props)
+        await this.plugin.runTask(state.updateTree(update, { doNotUpdateCurrent: true }))
+    }
+
     private _ignoreHydrogens = false
     get ignoreHydrogens () { return this._ignoreHydrogens }
     async setIgnoreHydrogens(ignoreHydrogens: boolean) {
         if (ignoreHydrogens === this._ignoreHydrogens) return
-        await this.eachRepresentation((repr, update) => {
+        await this.eachRepresentation((repr, type, update) => {
             if (repr.params && repr.params.values.type.params.ignoreHydrogens !== undefined) {
                 const { name, params } = repr.params.values.type
                 update.to(repr.transform.ref).update(
@@ -157,7 +228,7 @@ export class StructureRepresentationHelper {
     get quality () { return this._quality }
     async setQuality(quality: VisualQuality) {
         if (quality === this._quality) return
-        await this.eachRepresentation((repr, update) => {
+        await this.eachRepresentation((repr, type, update) => {
             if (repr.params && repr.params.values.type.params.quality !== undefined) {
                 const { name, params } = repr.params.values.type
                 update.to(repr.transform.ref).update(
@@ -169,74 +240,7 @@ export class StructureRepresentationHelper {
         this._quality = quality
     }
 
-    async preset() {
-        // TODO option to limit to specific structure
-        const state = this.plugin.state.dataState;
-        const structures = state.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Structure))
-
-        if (structures.length === 0) return
-        const s = structures[0].obj!.data
-
-        if (s.elementCount < 50000) {
-            await polymerAndLigand(this)
-        } else if (s.elementCount < 200000) {
-            await proteinAndNucleic(this)
-        } else {
-            if (s.unitSymmetryGroups[0].units.length > 10) {
-                await capsid(this)
-            } else {
-                await coarseCapsid(this)
-            }
-        }
-    }
-
     constructor(private plugin: PluginContext) {
 
     }
-}
-
-//
-
-async function polymerAndLigand(r: StructureRepresentationHelper) {
-    await r.clear()
-    await r.setFromExpression('add', 'cartoon', Q.polymer.expression)
-    await r.setFromExpression('add', 'carbohydrate', Q.branchedPlusConnected.expression)
-    await r.setFromExpression('add', 'ball-and-stick', MS.struct.modifier.union([
-        MS.struct.combinator.merge([
-            Q.ligandPlusConnected.expression,
-            Q.branchedConnectedOnly.expression,
-            Q.disulfideBridges.expression,
-            Q.nonStandardPolymer.expression,
-            Q.water.expression
-        ])
-    ]))
-}
-
-async function proteinAndNucleic(r: StructureRepresentationHelper) {
-    await r.clear()
-    await r.setFromExpression('add', 'cartoon', Q.protein.expression)
-    await r.setFromExpression('add', 'gaussian-surface', Q.nucleic.expression)
-}
-
-async function capsid(r: StructureRepresentationHelper) {
-    await r.clear()
-    await r.setFromExpression('add', 'gaussian-surface', Q.polymer.expression, {
-        smoothness: 0.5,
-    })
-}
-
-async function coarseCapsid(r: StructureRepresentationHelper) {
-    await r.clear()
-    await r.setFromExpression('add', 'gaussian-surface', Q.trace.expression, {
-        radiusOffset: 1,
-        smoothness: 0.5,
-        visuals: ['structure-gaussian-surface-mesh']
-    })
-}
-
-export const StructureRepresentationPresets = {
-    polymerAndLigand,
-    proteinAndNucleic,
-    capsid,
-    coarseCapsid
 }
\ No newline at end of file
diff --git a/src/mol-plugin/util/structure-selection-helper.ts b/src/mol-plugin/util/structure-selection-helper.ts
index e8b3c455305d0d85a04cb3d7f407d91abae352f2..377c6507a249752adc9d183299bcbc951afff6e9 100644
--- a/src/mol-plugin/util/structure-selection-helper.ts
+++ b/src/mol-plugin/util/structure-selection-helper.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -8,32 +8,76 @@
 import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
 import { StateSelection, StateBuilder } from '../../mol-state';
 import { PluginStateObject } from '../state/objects';
-import { QueryContext, StructureSelection, StructureQuery, StructureElement } from '../../mol-model/structure';
+import { QueryContext, StructureSelection, StructureQuery, StructureElement, Structure } from '../../mol-model/structure';
 import { compile } from '../../mol-script/runtime/query/compiler';
 import { Loci } from '../../mol-model/loci';
 import { PluginContext } from '../context';
 import Expression from '../../mol-script/language/expression';
 import { BondType, ProteinBackboneAtoms, NucleicBackboneAtoms, SecondaryStructureType } from '../../mol-model/structure/model/types';
 import { StateTransforms } from '../state/transforms';
+import { SetUtils } from '../../mol-util/set';
+import { ValidationReport, ValidationReportProvider } from '../../mol-model-props/rcsb/validation-report';
+import { CustomProperty } from '../../mol-model-props/common/custom-property';
+import { Task } from '../../mol-task';
+import { AccessibleSurfaceAreaSymbols, AccessibleSurfaceAreaProvider } from '../../mol-model-props/computed/accessible-surface-area';
+import { stringToWords } from '../../mol-util/string';
+
+export enum StructureSelectionCategory {
+    Type = 'Type',
+    Structure = 'Structure Property',
+    Atom = 'Atom Property',
+    Bond = 'Bond Property',
+    Residue = 'Residue Property',
+    AminoAcid = 'Amino Acid',
+    NucleicBase = 'Nucleic Base',
+    Manipulate = 'Manipulate Selection',
+    Validation = 'Validation',
+    Misc = 'Miscellaneous',
+    Internal = 'Internal',
+}
+
+export { StructureSelectionQuery }
+
+interface StructureSelectionQuery {
+    readonly label: string
+    readonly expression: Expression
+    readonly description: string
+    readonly category: string
+    readonly isHidden: boolean
+    readonly query: StructureQuery
+    readonly ensureCustomProperties?: (ctx: CustomProperty.Context, structure: Structure) => Promise<void>
+}
 
-export interface StructureSelectionQuery {
-    label: string
-    query: StructureQuery
-    expression: Expression
-    description: string
+interface StructureSelectionQueryProps {
+    description?: string,
+    category?: string
+    isHidden?: boolean
+    ensureCustomProperties?: (ctx: CustomProperty.Context, structure: Structure) => Promise<void>
 }
 
-export function StructureSelectionQuery(label: string, expression: Expression, description = ''): StructureSelectionQuery {
-    return { label, expression, query: compile<StructureSelection>(expression), description }
+function StructureSelectionQuery(label: string, expression: Expression, props: StructureSelectionQueryProps = {}): StructureSelectionQuery {
+    let _query: StructureQuery
+    return {
+        label,
+        expression,
+        description: props.description || '',
+        category: props.category ?? StructureSelectionCategory.Misc,
+        isHidden: !!props.isHidden,
+        get query() {
+            if (!_query) _query = compile<StructureSelection>(expression)
+            return _query
+        },
+        ensureCustomProperties: props.ensureCustomProperties
+    }
 }
 
-const all = StructureSelectionQuery('All', MS.struct.generator.all())
+const all = StructureSelectionQuery('All', MS.struct.generator.all(), { category: '' })
 
 const polymer = StructureSelectionQuery('Polymer', MS.struct.modifier.union([
     MS.struct.generator.atomGroups({
         'entity-test': MS.core.rel.eq([MS.ammp('entityType'), 'polymer'])
     })
-]))
+]), { category: StructureSelectionCategory.Type })
 
 const trace = StructureSelectionQuery('Trace', MS.struct.modifier.union([
     MS.struct.combinator.merge([
@@ -53,7 +97,7 @@ const trace = StructureSelectionQuery('Trace', MS.struct.modifier.union([
             })
         ])
     ])
-]))
+]), { category: StructureSelectionCategory.Structure })
 
 // TODO maybe pre-calculate atom properties like backbone/sidechain
 const backbone = StructureSelectionQuery('Backbone', MS.struct.modifier.union([
@@ -68,7 +112,7 @@ const backbone = StructureSelectionQuery('Backbone', MS.struct.modifier.union([
                     ])
                 ]),
                 'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
-                'atom-test': MS.core.set.has([MS.set(...Array.from(ProteinBackboneAtoms.values())), MS.ammp('label_atom_id')])
+                'atom-test': MS.core.set.has([MS.set(...SetUtils.toArray(ProteinBackboneAtoms)), MS.ammp('label_atom_id')])
             })
         ]),
         MS.struct.modifier.union([
@@ -81,11 +125,11 @@ const backbone = StructureSelectionQuery('Backbone', MS.struct.modifier.union([
                     ])
                 ]),
                 'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
-                'atom-test': MS.core.set.has([MS.set(...Array.from(NucleicBackboneAtoms.values())), MS.ammp('label_atom_id')])
+                'atom-test': MS.core.set.has([MS.set(...SetUtils.toArray(NucleicBackboneAtoms)), MS.ammp('label_atom_id')])
             })
         ])
     ])
-]))
+]), { category: StructureSelectionCategory.Structure })
 
 const protein = StructureSelectionQuery('Protein', MS.struct.modifier.union([
     MS.struct.generator.atomGroups({
@@ -97,7 +141,7 @@ const protein = StructureSelectionQuery('Protein', MS.struct.modifier.union([
             ])
         ])
     })
-]))
+]), { category: StructureSelectionCategory.Type })
 
 const nucleic = StructureSelectionQuery('Nucleic', MS.struct.modifier.union([
     MS.struct.generator.atomGroups({
@@ -109,7 +153,7 @@ const nucleic = StructureSelectionQuery('Nucleic', MS.struct.modifier.union([
             ])
         ])
     })
-]))
+]), { category: StructureSelectionCategory.Type })
 
 const proteinOrNucleic = StructureSelectionQuery('Protein or Nucleic', MS.struct.modifier.union([
     MS.struct.generator.atomGroups({
@@ -121,7 +165,7 @@ const proteinOrNucleic = StructureSelectionQuery('Protein or Nucleic', MS.struct
             ])
         ])
     })
-]))
+]), { category: StructureSelectionCategory.Type })
 
 const helix = StructureSelectionQuery('Helix', MS.struct.modifier.union([
     MS.struct.generator.atomGroups({
@@ -137,7 +181,7 @@ const helix = StructureSelectionQuery('Helix', MS.struct.modifier.union([
             MS.core.type.bitflags([SecondaryStructureType.Flag.Helix])
         ])
     })
-]))
+]), { category: StructureSelectionCategory.Residue })
 
 const beta = StructureSelectionQuery('Beta Strand/Sheet', MS.struct.modifier.union([
     MS.struct.generator.atomGroups({
@@ -153,13 +197,13 @@ const beta = StructureSelectionQuery('Beta Strand/Sheet', MS.struct.modifier.uni
             MS.core.type.bitflags([SecondaryStructureType.Flag.Beta])
         ])
     })
-]))
+]), { category: StructureSelectionCategory.Residue })
 
 const water = StructureSelectionQuery('Water', MS.struct.modifier.union([
     MS.struct.generator.atomGroups({
         'entity-test': MS.core.rel.eq([MS.ammp('entityType'), 'water'])
     })
-]))
+]), { category: StructureSelectionCategory.Type })
 
 const branched = StructureSelectionQuery('Carbohydrate', MS.struct.modifier.union([
     MS.struct.generator.atomGroups({
@@ -174,20 +218,20 @@ const branched = StructureSelectionQuery('Carbohydrate', MS.struct.modifier.unio
             ])
         ])
     })
-]))
+]), { category: StructureSelectionCategory.Type })
 
 const branchedPlusConnected = StructureSelectionQuery('Carbohydrate with Connected', MS.struct.modifier.union([
     MS.struct.modifier.includeConnected({
         0: branched.expression, 'layer-count': 1, 'as-whole-residues': true
     })
-]))
+]), { category: StructureSelectionCategory.Internal })
 
 const branchedConnectedOnly = StructureSelectionQuery('Connected to Carbohydrate', MS.struct.modifier.union([
     MS.struct.modifier.exceptBy({
         0: branchedPlusConnected.expression,
         by: branched.expression
     })
-]))
+]), { category: StructureSelectionCategory.Internal })
 
 const ligand = StructureSelectionQuery('Ligand', MS.struct.modifier.union([
     MS.struct.combinator.merge([
@@ -221,7 +265,7 @@ const ligand = StructureSelectionQuery('Ligand', MS.struct.modifier.union([
             })
         ])
     ]),
-]))
+]), { category: StructureSelectionCategory.Residue })
 
 // don't include branched entities as they have their own link representation
 const ligandPlusConnected = StructureSelectionQuery('Ligand with Connected', MS.struct.modifier.union([
@@ -241,14 +285,14 @@ const ligandPlusConnected = StructureSelectionQuery('Ligand with Connected', MS.
         ]),
         by: branched.expression
     })
-]))
+]), { category: StructureSelectionCategory.Internal })
 
 const ligandConnectedOnly = StructureSelectionQuery('Connected to Ligand', MS.struct.modifier.union([
     MS.struct.modifier.exceptBy({
         0: ligandPlusConnected.expression,
         by: ligand.expression
     })
-]))
+]), { category: StructureSelectionCategory.Internal })
 
 // residues connected to ligands or branched entities
 const connectedOnly = StructureSelectionQuery('Connected to Ligand or Carbohydrate', MS.struct.modifier.union([
@@ -256,7 +300,7 @@ const connectedOnly = StructureSelectionQuery('Connected to Ligand or Carbohydra
         branchedConnectedOnly.expression,
         ligandConnectedOnly.expression
     ]),
-]))
+]), { category: StructureSelectionCategory.Internal })
 
 const disulfideBridges = StructureSelectionQuery('Disulfide Bridges', MS.struct.modifier.union([
     MS.struct.modifier.wholeResidues([
@@ -269,14 +313,7 @@ const disulfideBridges = StructureSelectionQuery('Disulfide Bridges', MS.struct.
             })
         ])
     ])
-]))
-
-const modified = StructureSelectionQuery('Modified Residues', MS.struct.modifier.union([
-    MS.struct.generator.atomGroups({
-        'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
-        'residue-test': MS.ammp('isModified')
-    })
-]))
+]), { category: StructureSelectionCategory.Bond })
 
 const nonStandardPolymer = StructureSelectionQuery('Non-standard Residues in Polymers', MS.struct.modifier.union([
     MS.struct.generator.atomGroups({
@@ -284,7 +321,7 @@ const nonStandardPolymer = StructureSelectionQuery('Non-standard Residues in Pol
         'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
         'residue-test': MS.ammp('isNonStandard')
     })
-]))
+]), { category: StructureSelectionCategory.Residue })
 
 const coarse = StructureSelectionQuery('Coarse Elements', MS.struct.modifier.union([
     MS.struct.generator.atomGroups({
@@ -292,15 +329,15 @@ const coarse = StructureSelectionQuery('Coarse Elements', MS.struct.modifier.uni
             MS.set('sphere', 'gaussian'), MS.ammp('objectPrimitive')
         ])
     })
-]))
+]), { category: StructureSelectionCategory.Residue })
 
 const ring = StructureSelectionQuery('Rings in Residues', MS.struct.modifier.union([
     MS.struct.generator.rings()
-]))
+]), { category: StructureSelectionCategory.Residue })
 
 const aromaticRing = StructureSelectionQuery('Aromatic Rings in Residues', MS.struct.modifier.union([
     MS.struct.generator.rings({ 'only-aromatic': true })
-]))
+]), { category: StructureSelectionCategory.Residue })
 
 const surroundings = StructureSelectionQuery('Surrounding Residues (5 \u212B) of Selection', MS.struct.modifier.union([
     MS.struct.modifier.exceptBy({
@@ -311,20 +348,121 @@ const surroundings = StructureSelectionQuery('Surrounding Residues (5 \u212B) of
         }),
         by: MS.internal.generator.current()
     })
-]), 'Select residues within 5 \u212B of the current selection.')
+]), {
+    description: 'Select residues within 5 \u212B of the current selection.',
+    category: StructureSelectionCategory.Manipulate
+})
 
 const complement = StructureSelectionQuery('Inverse / Complement of Selection', MS.struct.modifier.union([
     MS.struct.modifier.exceptBy({
         0: MS.struct.generator.all(),
         by: MS.internal.generator.current()
     })
-]), 'Select everything not in the current selection.')
+]), {
+    description: 'Select everything not in the current selection.',
+    category: StructureSelectionCategory.Manipulate
+})
 
 const bonded = StructureSelectionQuery('Residues Bonded to Selection', MS.struct.modifier.union([
     MS.struct.modifier.includeConnected({
         0: MS.internal.generator.current(), 'layer-count': 1, 'as-whole-residues': true
     })
-]), 'Select residues covalently bonded to current selection.')
+]), {
+    description: 'Select residues covalently bonded to current selection.',
+    category: StructureSelectionCategory.Manipulate
+})
+
+const hasClash = StructureSelectionQuery('Residues with Clashes', MS.struct.modifier.union([
+    MS.struct.modifier.wholeResidues([
+        MS.struct.modifier.union([
+            MS.struct.generator.atomGroups({
+                'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
+                'atom-test': ValidationReport.symbols.hasClash.symbol(),
+            })
+        ])
+    ])
+]), {
+    description: 'Select residues with clashes in the wwPDB validation report.',
+    category: StructureSelectionCategory.Residue,
+    ensureCustomProperties: (ctx, structure) => {
+        return ValidationReportProvider.attach(ctx, structure.models[0])
+    }
+})
+
+const isBuried = StructureSelectionQuery('Buried Protein Residues', MS.struct.modifier.union([
+    MS.struct.modifier.wholeResidues([
+        MS.struct.modifier.union([
+            MS.struct.generator.atomGroups({
+                'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
+                'residue-test': AccessibleSurfaceAreaSymbols.isBuried.symbol(),
+            })
+        ])
+    ])
+]), {
+    description: 'Select buried protein residues.',
+    category: StructureSelectionCategory.Residue,
+    ensureCustomProperties: (ctx, structure) => {
+        return AccessibleSurfaceAreaProvider.attach(ctx, structure)
+    }
+})
+
+const isAccessible = StructureSelectionQuery('Accessible Protein Residues', MS.struct.modifier.union([
+    MS.struct.modifier.wholeResidues([
+        MS.struct.modifier.union([
+            MS.struct.generator.atomGroups({
+                'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
+                'residue-test': AccessibleSurfaceAreaSymbols.isAccessible.symbol(),
+            })
+        ])
+    ])
+]), {
+    description: 'Select accessible protein residues.',
+    category: StructureSelectionCategory.Residue,
+    ensureCustomProperties: (ctx, structure) => {
+        return AccessibleSurfaceAreaProvider.attach(ctx, structure)
+    }
+})
+
+const StandardAminoAcids = [
+    [['HIS'], 'HISTIDINE'],
+    [['ARG'], 'ARGININE'],
+    [['LYS'], 'LYSINE'],
+    [['ILE'], 'ISOLEUCINE'],
+    [['PHE'], 'PHENYLALANINE'],
+    [['LEU'], 'LEUCINE'],
+    [['TRP'], 'TRYPTOPHAN'],
+    [['ALA'], 'ALANINE'],
+    [['MET'], 'METHIONINE'],
+    [['CYS'], 'CYSTEINE'],
+    [['ASN'], 'ASPARAGINE'],
+    [['VAL'], 'VALINE'],
+    [['GLY'], 'GLYCINE'],
+    [['SER'], 'SERINE'],
+    [['GLN'], 'GLUTAMINE'],
+    [['TYR'], 'TYROSINE'],
+    [['ASP'], 'ASPARTIC ACID'],
+    [['GLU'], 'GLUTAMIC ACID'],
+    [['THR'], 'THREONINE'],
+    [['SEC'], 'SELENOCYSTEINE'],
+    [['PYL'], 'PYRROLYSINE'],
+].sort((a, b) => a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0) as [string[], string][]
+
+const StandardNucleicBases = [
+    [['A', 'DA'], 'ADENOSINE'],
+    [['C', 'DC'], 'CYTIDINE'],
+    [['T', 'DT'], 'THYMIDINE'],
+    [['G', 'DG'], 'GUANOSINE'],
+    [['I', 'DI'], 'INOSINE'],
+    [['U', 'DU'], 'URIDINE'],
+].sort((a, b) => a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0) as [string[], string][]
+
+function ResidueQuery([names, label]: [string[], string], category: string) {
+    return StructureSelectionQuery(`${stringToWords(label)} (${names.join(', ')})`, MS.struct.modifier.union([
+        MS.struct.generator.atomGroups({
+            'residue-test': MS.core.set.has([MS.set(...names), MS.ammp('auth_comp_id')])
+        })
+    ]), { category })
+}
 
 export const StructureSelectionQueries = {
     all,
@@ -345,7 +483,6 @@ export const StructureSelectionQueries = {
     ligandConnectedOnly,
     connectedOnly,
     disulfideBridges,
-    modified,
     nonStandardPolymer,
     coarse,
     ring,
@@ -353,8 +490,18 @@ export const StructureSelectionQueries = {
     surroundings,
     complement,
     bonded,
+
+    hasClash,
+    isBuried,
+    isAccessible
 }
 
+export const StructureSelectionQueryList = [
+    ...Object.values(StructureSelectionQueries),
+    ...StandardAminoAcids.map(v => ResidueQuery(v, StructureSelectionCategory.AminoAcid)),
+    ...StandardNucleicBases.map(v => ResidueQuery(v, StructureSelectionCategory.NucleicBase)),
+]
+
 export function applyBuiltInSelection(to: StateBuilder.To<PluginStateObject.Molecule.Structure>, query: keyof typeof StructureSelectionQueries, customTag?: string) {
     return to.apply(StateTransforms.Model.StructureSelectionFromExpression,
         { expression: StructureSelectionQueries[query].expression, label: StructureSelectionQueries[query].label },
@@ -384,17 +531,24 @@ export class StructureSelectionHelper {
         }
     }
 
-    set(modifier: SelectionModifier, query: StructureQuery, applyGranularity = true) {
-        for (const s of this.structures) {
-            const current = this.plugin.helpers.structureSelectionManager.get(s)
-            const currentSelection = Loci.isEmpty(current)
-                ? StructureSelection.Empty(s)
-                : StructureSelection.Singletons(s, StructureElement.Loci.toStructure(current))
-
-            const result = query(new QueryContext(s, { currentSelection }))
-            const loci = StructureSelection.toLociWithSourceUnits(result)
-            this._set(modifier, loci, applyGranularity)
-        }
+    async set(modifier: SelectionModifier, selectionQuery: StructureSelectionQuery, applyGranularity = true) {
+        this.plugin.runTask(Task.create('Structure Selection', async runtime => {
+            const ctx = { fetch: this.plugin.fetch, runtime }
+            for (const s of this.structures) {
+                const current = this.plugin.helpers.structureSelectionManager.get(s)
+                const currentSelection = Loci.isEmpty(current)
+                    ? StructureSelection.Empty(s)
+                    : StructureSelection.Singletons(s, StructureElement.Loci.toStructure(current))
+
+                if (selectionQuery.ensureCustomProperties) {
+                    await selectionQuery.ensureCustomProperties(ctx, s)
+                }
+
+                const result = selectionQuery.query(new QueryContext(s, { currentSelection }))
+                const loci = StructureSelection.toLociWithSourceUnits(result)
+                this._set(modifier, loci, applyGranularity)
+            }
+        }))
     }
 
     constructor(private plugin: PluginContext) {
diff --git a/src/mol-repr/structure/registry.ts b/src/mol-repr/structure/registry.ts
index 970c5e12bc299113d8865aac03aa6dbb16fc3835..9ec7a53d1263c59f042a1a0ee4686e106ffebb70 100644
--- a/src/mol-repr/structure/registry.ts
+++ b/src/mol-repr/structure/registry.ts
@@ -11,7 +11,6 @@ import { BallAndStickRepresentationProvider } from './representation/ball-and-st
 import { GaussianSurfaceRepresentationProvider } from './representation/gaussian-surface';
 import { CarbohydrateRepresentationProvider } from './representation/carbohydrate';
 import { SpacefillRepresentationProvider } from './representation/spacefill';
-import { DistanceRestraintRepresentationProvider } from './representation/distance-restraint';
 import { PointRepresentationProvider } from './representation/point';
 import { StructureRepresentationState } from './representation';
 import { PuttyRepresentationProvider } from './representation/putty';
@@ -34,7 +33,6 @@ export const BuiltInStructureRepresentations = {
     'cartoon': CartoonRepresentationProvider,
     'ball-and-stick': BallAndStickRepresentationProvider,
     'carbohydrate': CarbohydrateRepresentationProvider,
-    'distance-restraint': DistanceRestraintRepresentationProvider,
     'ellipsoid': EllipsoidRepresentationProvider,
     'gaussian-surface': GaussianSurfaceRepresentationProvider,
     // 'gaussian-volume': GaussianVolumeRepresentationProvider, // TODO disabled for now, needs more work
diff --git a/src/mol-repr/structure/representation/distance-restraint.ts b/src/mol-repr/structure/representation/distance-restraint.ts
deleted file mode 100644
index 752a9b46d0f1d0ffb50bcf4bc81b199fbe88dd20..0000000000000000000000000000000000000000
--- a/src/mol-repr/structure/representation/distance-restraint.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { CrossLinkRestraintVisual, CrossLinkRestraintParams } from '../visual/cross-link-restraint-cylinder';
-import { ParamDefinition as PD } from '../../../mol-util/param-definition';
-import { ComplexRepresentation } from '../complex-representation';
-import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder } from '../representation';
-import { Representation, RepresentationContext, RepresentationParamsGetter } from '../../../mol-repr/representation';
-import { ThemeRegistryContext } from '../../../mol-theme/theme';
-import { Structure } from '../../../mol-model/structure';
-import { UnitKind, UnitKindOptions } from '../visual/util/common';
-
-const DistanceRestraintVisuals = {
-    'distance-restraint': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, CrossLinkRestraintParams>) => ComplexRepresentation('Cross-link restraint', ctx, getParams, CrossLinkRestraintVisual),
-}
-
-export const DistanceRestraintParams = {
-    ...CrossLinkRestraintParams,
-    unitKinds: PD.MultiSelect<UnitKind>(['atomic', 'spheres'], UnitKindOptions),
-}
-export type DistanceRestraintParams = typeof DistanceRestraintParams
-export function getDistanceRestraintParams(ctx: ThemeRegistryContext, structure: Structure) {
-    return PD.clone(DistanceRestraintParams)
-}
-
-export type DistanceRestraintRepresentation = StructureRepresentation<DistanceRestraintParams>
-export function DistanceRestraintRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, DistanceRestraintParams>): DistanceRestraintRepresentation {
-    return Representation.createMulti('DistanceRestraint', ctx, getParams, StructureRepresentationStateBuilder, DistanceRestraintVisuals as unknown as Representation.Def<Structure, DistanceRestraintParams>)
-}
-
-export const DistanceRestraintRepresentationProvider: StructureRepresentationProvider<DistanceRestraintParams> = {
-    label: 'Distance Restraint',
-    description: 'Displays cross-link distance restraints.',
-    factory: DistanceRestraintRepresentation,
-    getParams: getDistanceRestraintParams,
-    defaultValues: PD.getDefaultValues(DistanceRestraintParams),
-    defaultColorTheme: { name: 'cross-link' },
-    defaultSizeTheme: { name: 'uniform' },
-    isApplicable: (structure: Structure) => structure.crossLinkRestraints.count > 0
-}
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts b/src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts
deleted file mode 100644
index db047214479a432ea8d0e84dc57c3f5d9b3fd0d1..0000000000000000000000000000000000000000
--- a/src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-/**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { ParamDefinition as PD } from '../../../mol-util/param-definition';
-import { VisualContext } from '../../visual';
-import { Structure, StructureElement, Bond } from '../../../mol-model/structure';
-import { Theme } from '../../../mol-theme/theme';
-import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
-import { Vec3 } from '../../../mol-math/linear-algebra';
-import { createLinkCylinderMesh, LinkCylinderParams } from './util/link';
-import { ComplexMeshParams, ComplexVisual, ComplexMeshVisual } from '../complex-visual';
-import { VisualUpdateState } from '../../util';
-import { LocationIterator } from '../../../mol-geo/util/location-iterator';
-import { PickingId } from '../../../mol-geo/geometry/picking';
-import { EmptyLoci, Loci } from '../../../mol-model/loci';
-import { Interval } from '../../../mol-data/int';
-
-function createCrossLinkRestraintCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<CrossLinkRestraintParams>, mesh?: Mesh) {
-
-    const crossLinks = structure.crossLinkRestraints
-    if (!crossLinks.count) return Mesh.createEmpty(mesh)
-    const { sizeFactor } = props
-
-    const location = StructureElement.Location.create(structure)
-
-    const builderProps = {
-        linkCount: crossLinks.count,
-        position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
-            const b = crossLinks.pairs[edgeIndex]
-            const uA = b.unitA, uB = b.unitB
-            uA.conformation.position(uA.elements[b.indexA], posA)
-            uB.conformation.position(uB.elements[b.indexB], posB)
-        },
-        radius: (edgeIndex: number) => {
-            const b = crossLinks.pairs[edgeIndex]
-            location.unit = b.unitA
-            location.element = b.unitA.elements[b.indexA]
-            return theme.size.size(location) * sizeFactor
-        },
-    }
-
-    return createLinkCylinderMesh(ctx, builderProps, props, mesh)
-}
-
-export const CrossLinkRestraintParams = {
-    ...ComplexMeshParams,
-    ...LinkCylinderParams,
-    sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }),
-}
-export type CrossLinkRestraintParams = typeof CrossLinkRestraintParams
-
-export function CrossLinkRestraintVisual(materialId: number): ComplexVisual<CrossLinkRestraintParams> {
-    return ComplexMeshVisual<CrossLinkRestraintParams>({
-        defaultProps: PD.getDefaultValues(CrossLinkRestraintParams),
-        createGeometry: createCrossLinkRestraintCylinderMesh,
-        createLocationIterator: CrossLinkRestraintIterator,
-        getLoci: getLinkLoci,
-        eachLocation: eachCrossLink,
-        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<CrossLinkRestraintParams>, currentProps: PD.Values<CrossLinkRestraintParams>) => {
-            state.createGeometry = (
-                newProps.sizeFactor !== currentProps.sizeFactor ||
-                newProps.radialSegments !== currentProps.radialSegments ||
-                newProps.linkCap !== currentProps.linkCap
-            )
-        }
-    }, materialId)
-}
-
-function CrossLinkRestraintIterator(structure: Structure): LocationIterator {
-    const { pairs } = structure.crossLinkRestraints
-    const groupCount = pairs.length
-    const instanceCount = 1
-    const location = Bond.Location()
-    const getLocation = (groupIndex: number) => {
-        const pair = pairs[groupIndex]
-        location.aStructure = structure
-        location.aUnit = pair.unitA
-        location.aIndex = pair.indexA
-        location.bStructure = structure
-        location.bUnit = pair.unitB
-        location.bIndex = pair.indexB
-        return location
-    }
-    return LocationIterator(groupCount, instanceCount, getLocation, true)
-}
-
-function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) {
-    const { objectId, groupId } = pickingId
-    if (id === objectId) {
-        const pair = structure.crossLinkRestraints.pairs[groupId]
-        if (pair) {
-            return Bond.Loci(structure, [
-                Bond.Location(structure, pair.unitA, pair.indexA, structure, pair.unitB, pair.indexB),
-                Bond.Location(structure, pair.unitB, pair.indexB, structure, pair.unitA, pair.indexA)
-            ])
-        }
-    }
-    return EmptyLoci
-}
-
-function eachCrossLink(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
-    const crossLinks = structure.crossLinkRestraints
-    let changed = false
-    if (Bond.isLoci(loci)) {
-        if (!Structure.areEquivalent(loci.structure, structure)) return false
-        for (const b of loci.bonds) {
-            const indices = crossLinks.getPairIndices(b.aIndex, b.aUnit, b.bIndex, b.bUnit)
-            if (indices) {
-                for (let i = 0, il = indices.length; i < il; ++i) {
-                    if (apply(Interval.ofSingleton(indices[i]))) changed = true
-                }
-            }
-        }
-    }
-    return changed
-}
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/nucleotide-block-mesh.ts b/src/mol-repr/structure/visual/nucleotide-block-mesh.ts
index 255001edbe5f4f32d766b2506c72e33d317e7fa5..95b701f2c222eef56b850df1ea6d09f48d8ebdf6 100644
--- a/src/mol-repr/structure/visual/nucleotide-block-mesh.ts
+++ b/src/mol-repr/structure/visual/nucleotide-block-mesh.ts
@@ -53,7 +53,6 @@ function createNucleotideBlockMesh(ctx: VisualContext, unit: Unit, structure: St
     const builderState = MeshBuilder.createState(vertexCount, vertexCount / 4, mesh)
 
     const { elements, model } = unit
-    const { modifiedResidues } = model.properties
     const { chainAtomSegments, residueAtomSegments, residues, index: atomicIndex } = model.atomicHierarchy
     const { moleculeType, traceElementIndex } = model.atomicHierarchy.derived.residue
     const { label_comp_id } = residues
@@ -72,9 +71,7 @@ function createNucleotideBlockMesh(ctx: VisualContext, unit: Unit, structure: St
             const { index: residueIndex } = residueIt.move();
 
             if (isNucleic(moleculeType[residueIndex])) {
-                let compId = label_comp_id.value(residueIndex)
-                const parentId = modifiedResidues.parentId.get(compId)
-                if (parentId !== undefined) compId = parentId
+                const compId = label_comp_id.value(residueIndex)
                 let idx1: ElementIndex | -1 = -1, idx2: ElementIndex | -1 = -1, idx3: ElementIndex | -1 = -1, idx4: ElementIndex | -1 = -1, idx5: ElementIndex | -1 = -1, idx6: ElementIndex | -1 = -1
                 let width = 4.5, height = 4.5, depth = 2.5 * sizeFactor
 
diff --git a/src/mol-repr/structure/visual/nucleotide-ring-mesh.ts b/src/mol-repr/structure/visual/nucleotide-ring-mesh.ts
index c205c3f5af71e495c8a23a30ac560dbd134b5d73..2dd871368e68d2040ba15f7ce17d8215cd7f8e9d 100644
--- a/src/mol-repr/structure/visual/nucleotide-ring-mesh.ts
+++ b/src/mol-repr/structure/visual/nucleotide-ring-mesh.ts
@@ -72,7 +72,6 @@ function createNucleotideRingMesh(ctx: VisualContext, unit: Unit, structure: Str
     const builderState = MeshBuilder.createState(vertexCount, vertexCount / 4, mesh)
 
     const { elements, model } = unit
-    const { modifiedResidues } = model.properties
     const { chainAtomSegments, residueAtomSegments, residues, index: atomicIndex } = model.atomicHierarchy
     const { moleculeType, traceElementIndex } = model.atomicHierarchy.derived.residue
     const { label_comp_id } = residues
@@ -93,9 +92,7 @@ function createNucleotideRingMesh(ctx: VisualContext, unit: Unit, structure: Str
             const { index: residueIndex } = residueIt.move();
 
             if (isNucleic(moleculeType[residueIndex])) {
-                let compId = label_comp_id.value(residueIndex)
-                const parentId = modifiedResidues.parentId.get(compId)
-                if (parentId !== undefined) compId = parentId
+                const compId = label_comp_id.value(residueIndex)
 
                 let idxTrace: ElementIndex | -1 = -1, idxN1: ElementIndex | -1 = -1, idxC2: ElementIndex | -1 = -1, idxN3: ElementIndex | -1 = -1, idxC4: ElementIndex | -1 = -1, idxC5: ElementIndex | -1 = -1, idxC6: ElementIndex | -1 = -1, idxN7: ElementIndex | -1 = -1, idxC8: ElementIndex | -1 = -1, idxN9: ElementIndex | -1 = -1
 
diff --git a/src/mol-script/runtime/query/table.ts b/src/mol-script/runtime/query/table.ts
index 390e0c57a1051c2eff7c4a63268803e1a39b2df0..2046a6bb472b1066350284515affb13749057b21 100644
--- a/src/mol-script/runtime/query/table.ts
+++ b/src/mol-script/runtime/query/table.ts
@@ -325,8 +325,6 @@ const symbols = [
     D(MolScript.structureQuery.atomProperty.macromolecular.entitySubtype, atomProp(StructureProperties.entity.subtype)),
     D(MolScript.structureQuery.atomProperty.macromolecular.objectPrimitive, atomProp(StructureProperties.unit.object_primitive)),
 
-    D(MolScript.structureQuery.atomProperty.macromolecular.isModified, atomProp(StructureProperties.residue.isModified)),
-    D(MolScript.structureQuery.atomProperty.macromolecular.modifiedParentName, atomProp(StructureProperties.residue.modifiedParentName)),
     D(MolScript.structureQuery.atomProperty.macromolecular.isNonStandard, atomProp(StructureProperties.residue.isNonStandard)),
     D(MolScript.structureQuery.atomProperty.macromolecular.secondaryStructureKey, atomProp(StructureProperties.residue.secondary_structure_key)),
     D(MolScript.structureQuery.atomProperty.macromolecular.secondaryStructureFlags, atomProp(StructureProperties.residue.secondary_structure_type)),
diff --git a/src/mol-theme/color.ts b/src/mol-theme/color.ts
index 9288607d6d919b675f76a7ddd2b686f63c5bbb55..6829233f6e18fe8642b1216451c6ad8bd7f8c84f 100644
--- a/src/mol-theme/color.ts
+++ b/src/mol-theme/color.ts
@@ -13,7 +13,6 @@ import { deepEqual } from '../mol-util';
 import { ParamDefinition as PD } from '../mol-util/param-definition';
 import { ThemeDataContext, ThemeRegistry, ThemeProvider } from './theme';
 import { ChainIdColorThemeProvider } from './color/chain-id';
-import { CrossLinkColorThemeProvider } from './color/cross-link';
 import { ElementIndexColorThemeProvider } from './color/element-index';
 import { ElementSymbolColorThemeProvider } from './color/element-symbol';
 import { MoleculeTypeColorThemeProvider } from './color/molecule-type';
@@ -47,6 +46,15 @@ interface ColorTheme<P extends PD.Params> {
     readonly legend?: Readonly<ScaleLegend | TableLegend>
 }
 namespace ColorTheme {
+    export const enum Category {
+        Atom = 'Atom Property',
+        Chain = 'Chain Property',
+        Residue = 'Residue Property',
+        Symmetry = 'Symmetry',
+        Validation = 'Validation',
+        Misc = 'Miscellaneous',
+    }
+
     export type Props = { [k: string]: any }
     export type Factory<P extends PD.Params> = (ctx: ThemeDataContext, props: PD.Values<P>) => ColorTheme<P>
     export const EmptyFactory = () => Empty
@@ -63,7 +71,7 @@ namespace ColorTheme {
     }
 
     export interface Provider<P extends PD.Params> extends ThemeProvider<ColorTheme<P>, P> { }
-    export const EmptyProvider: Provider<{}> = { label: '', factory: EmptyFactory, getParams: () => ({}), defaultValues: {}, isApplicable: () => true }
+    export const EmptyProvider: Provider<{}> = { label: '', category: '', factory: EmptyFactory, getParams: () => ({}), defaultValues: {}, isApplicable: () => true }
 
     export type Registry = ThemeRegistry<ColorTheme<any>>
     export function createRegistry() {
@@ -76,7 +84,6 @@ namespace ColorTheme {
 export const BuiltInColorThemes = {
     'carbohydrate-symbol': CarbohydrateSymbolColorThemeProvider,
     'chain-id': ChainIdColorThemeProvider,
-    'cross-link': CrossLinkColorThemeProvider,
     'element-index': ElementIndexColorThemeProvider,
     'element-symbol': ElementSymbolColorThemeProvider,
     'entity-source': EntitySourceColorThemeProvider,
diff --git a/src/mol-theme/color/carbohydrate-symbol.ts b/src/mol-theme/color/carbohydrate-symbol.ts
index f29d10170f11dffba9626bc8ac4a212e68db3d94..39d41725491f48c9bf5ee3fcccdcc363d401d4c5 100644
--- a/src/mol-theme/color/carbohydrate-symbol.ts
+++ b/src/mol-theme/color/carbohydrate-symbol.ts
@@ -62,6 +62,7 @@ export function CarbohydrateSymbolColorTheme(ctx: ThemeDataContext, props: PD.Va
 
 export const CarbohydrateSymbolColorThemeProvider: ColorTheme.Provider<CarbohydrateSymbolColorThemeParams> = {
     label: 'Carbohydrate Symbol',
+    category: ColorTheme.Category.Residue,
     factory: CarbohydrateSymbolColorTheme,
     getParams: getCarbohydrateSymbolColorThemeParams,
     defaultValues: PD.getDefaultValues(CarbohydrateSymbolColorThemeParams),
diff --git a/src/mol-theme/color/chain-id.ts b/src/mol-theme/color/chain-id.ts
index 16c072ff40491cae1f2c181df6d4eef3194fe1fa..46b587c708517f24ee6c052fe6f7c47376c21a87 100644
--- a/src/mol-theme/color/chain-id.ts
+++ b/src/mol-theme/color/chain-id.ts
@@ -119,6 +119,7 @@ export function ChainIdColorTheme(ctx: ThemeDataContext, props: PD.Values<ChainI
 
 export const ChainIdColorThemeProvider: ColorTheme.Provider<ChainIdColorThemeParams> = {
     label: 'Chain Id',
+    category: ColorTheme.Category.Chain,
     factory: ChainIdColorTheme,
     getParams: getChainIdColorThemeParams,
     defaultValues: PD.getDefaultValues(ChainIdColorThemeParams),
diff --git a/src/mol-theme/color/element-index.ts b/src/mol-theme/color/element-index.ts
index be94f3376dd57a6609497ea829522055eff3e7d2..0e796512a00b6af51a604b3a92276aa49f1bf17f 100644
--- a/src/mol-theme/color/element-index.ts
+++ b/src/mol-theme/color/element-index.ts
@@ -73,6 +73,7 @@ export function ElementIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<E
 
 export const ElementIndexColorThemeProvider: ColorTheme.Provider<ElementIndexColorThemeParams> = {
     label: 'Element Index',
+    category: ColorTheme.Category.Atom,
     factory: ElementIndexColorTheme,
     getParams: getElementIndexColorThemeParams,
     defaultValues: PD.getDefaultValues(ElementIndexColorThemeParams),
diff --git a/src/mol-theme/color/element-symbol.ts b/src/mol-theme/color/element-symbol.ts
index 6f2f8881f7d016719366e50f9c3730ad96ed996d..3582d1414f04afaac51849cf3cd262c4b8c6af71 100644
--- a/src/mol-theme/color/element-symbol.ts
+++ b/src/mol-theme/color/element-symbol.ts
@@ -69,6 +69,7 @@ export function ElementSymbolColorTheme(ctx: ThemeDataContext, props: PD.Values<
 
 export const ElementSymbolColorThemeProvider: ColorTheme.Provider<ElementSymbolColorThemeParams> = {
     label: 'Element Symbol',
+    category: ColorTheme.Category.Atom,
     factory: ElementSymbolColorTheme,
     getParams: getElementSymbolColorThemeParams,
     defaultValues: PD.getDefaultValues(ElementSymbolColorThemeParams),
diff --git a/src/mol-theme/color/entity-source.ts b/src/mol-theme/color/entity-source.ts
index a8ac312bfe1693d8be1637d780f1709702303b5d..641839576b59aba23a4e8ac23672aa9150928f64 100644
--- a/src/mol-theme/color/entity-source.ts
+++ b/src/mol-theme/color/entity-source.ts
@@ -175,6 +175,7 @@ export function EntitySourceColorTheme(ctx: ThemeDataContext, props: PD.Values<E
 
 export const EntitySourceColorThemeProvider: ColorTheme.Provider<EntitySourceColorThemeParams> = {
     label: 'Entity Source',
+    category: ColorTheme.Category.Chain,
     factory: EntitySourceColorTheme,
     getParams: getEntitySourceColorThemeParams,
     defaultValues: PD.getDefaultValues(EntitySourceColorThemeParams),
diff --git a/src/mol-theme/color/hydrophobicity.ts b/src/mol-theme/color/hydrophobicity.ts
index b281987b2d1d828456a8c39f075f74d68f69c6ca..6ddea14a6e505d8270eca238da5ec61450a8fc6e 100644
--- a/src/mol-theme/color/hydrophobicity.ts
+++ b/src/mol-theme/color/hydrophobicity.ts
@@ -32,22 +32,16 @@ export function hydrophobicity(compId: string, scaleIndex: number): number {
 }
 
 function getAtomicCompId(unit: Unit.Atomic, element: ElementIndex) {
-    const { modifiedResidues } = unit.model.properties
-    const compId = unit.model.atomicHierarchy.residues.label_comp_id.value(unit.residueIndex[element])
-    const parentId = modifiedResidues.parentId.get(compId)
-    return parentId === undefined ? compId : parentId
+    return unit.model.atomicHierarchy.residues.label_comp_id.value(unit.residueIndex[element])
 }
 
 function getCoarseCompId(unit: Unit.Spheres | Unit.Gaussians, element: ElementIndex) {
     const seqIdBegin = unit.coarseElements.seq_id_begin.value(element)
     const seqIdEnd = unit.coarseElements.seq_id_end.value(element)
     if (seqIdBegin === seqIdEnd) {
-        const { modifiedResidues } = unit.model.properties
         const entityKey = unit.coarseElements.entityKey[element]
         const seq = unit.model.sequence.byEntityKey[entityKey].sequence
-        let compId = seq.compId.value(seqIdBegin - 1) // 1-indexed
-        const parentId = modifiedResidues.parentId.get(compId)
-        return parentId === undefined ? compId : parentId
+        return seq.compId.value(seqIdBegin - 1) // 1-indexed
     }
 }
 
@@ -100,6 +94,7 @@ export function HydrophobicityColorTheme(ctx: ThemeDataContext, props: PD.Values
 
 export const HydrophobicityColorThemeProvider: ColorTheme.Provider<HydrophobicityColorThemeParams> = {
     label: 'Hydrophobicity',
+    category: ColorTheme.Category.Residue,
     factory: HydrophobicityColorTheme,
     getParams: getHydrophobicityColorThemeParams,
     defaultValues: PD.getDefaultValues(HydrophobicityColorThemeParams),
diff --git a/src/mol-theme/color/illustrative.ts b/src/mol-theme/color/illustrative.ts
index 6025f1eed563a09a307540d332fc67f3f4583bd7..9b86d8b97a76e5ac85504745ffb30c46fa28b17a 100644
--- a/src/mol-theme/color/illustrative.ts
+++ b/src/mol-theme/color/illustrative.ts
@@ -76,6 +76,7 @@ export function IllustrativeColorTheme(ctx: ThemeDataContext, props: PD.Values<I
 
 export const IllustrativeColorThemeProvider: ColorTheme.Provider<IllustrativeColorThemeParams> = {
     label: 'Illustrative',
+    category: ColorTheme.Category.Misc,
     factory: IllustrativeColorTheme,
     getParams: getIllustrativeColorThemeParams,
     defaultValues: PD.getDefaultValues(IllustrativeColorThemeParams),
diff --git a/src/mol-theme/color/model-index.ts b/src/mol-theme/color/model-index.ts
index fd98c44f77c4797a3710b20f0313f8969e25b899..20df7e57212ef604707c73e3fd707f4f8f82109b 100644
--- a/src/mol-theme/color/model-index.ts
+++ b/src/mol-theme/color/model-index.ts
@@ -61,6 +61,7 @@ export function ModelIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<Mod
 
 export const ModelIndexColorThemeProvider: ColorTheme.Provider<ModelIndexColorThemeParams> = {
     label: 'Model Index',
+    category: ColorTheme.Category.Chain,
     factory: ModelIndexColorTheme,
     getParams: getModelIndexColorThemeParams,
     defaultValues: PD.getDefaultValues(ModelIndexColorThemeParams),
diff --git a/src/mol-theme/color/molecule-type.ts b/src/mol-theme/color/molecule-type.ts
index 49f891381cf11888e0264233757fc4051bdc2707..1692e649728c5564deef6859b770c0bd6677e060 100644
--- a/src/mol-theme/color/molecule-type.ts
+++ b/src/mol-theme/color/molecule-type.ts
@@ -78,6 +78,7 @@ export function MoleculeTypeColorTheme(ctx: ThemeDataContext, props: PD.Values<M
 
 export const MoleculeTypeColorThemeProvider: ColorTheme.Provider<MoleculeTypeColorThemeParams> = {
     label: 'Molecule Type',
+    category: ColorTheme.Category.Residue,
     factory: MoleculeTypeColorTheme,
     getParams: getMoleculeTypeColorThemeParams,
     defaultValues: PD.getDefaultValues(MoleculeTypeColorThemeParams),
diff --git a/src/mol-theme/color/occupancy.ts b/src/mol-theme/color/occupancy.ts
index cf609f010a192f7adad85f8c1846d376b30e2627..47686503c292c9da785e2fd077b2617de628be14 100644
--- a/src/mol-theme/color/occupancy.ts
+++ b/src/mol-theme/color/occupancy.ts
@@ -60,6 +60,7 @@ export function OccupancyColorTheme(ctx: ThemeDataContext, props: PD.Values<Occu
 
 export const OccupancyColorThemeProvider: ColorTheme.Provider<OccupancyColorThemeParams> = {
     label: 'Occupancy',
+    category: ColorTheme.Category.Atom,
     factory: OccupancyColorTheme,
     getParams: getOccupancyColorThemeParams,
     defaultValues: PD.getDefaultValues(OccupancyColorThemeParams),
diff --git a/src/mol-theme/color/operator-hkl.ts b/src/mol-theme/color/operator-hkl.ts
index 2469356eaa12799c0496bf2f27cad96a04184311..b6adab5f2161bd87dd6eb2c6a8fe864295714184 100644
--- a/src/mol-theme/color/operator-hkl.ts
+++ b/src/mol-theme/color/operator-hkl.ts
@@ -119,6 +119,7 @@ export function OperatorHklColorTheme(ctx: ThemeDataContext, props: PD.Values<Op
 
 export const OperatorHklColorThemeProvider: ColorTheme.Provider<OperatorHklColorThemeParams> = {
     label: 'Operator HKL',
+    category: ColorTheme.Category.Symmetry,
     factory: OperatorHklColorTheme,
     getParams: getOperatorHklColorThemeParams,
     defaultValues: PD.getDefaultValues(OperatorHklColorThemeParams),
diff --git a/src/mol-theme/color/operator-name.ts b/src/mol-theme/color/operator-name.ts
index 16d48b147f4833878b6bd90452f70ac5dbb5a06d..3c4b60895f0dabda9af8c4bf6a817ed6f11d5830 100644
--- a/src/mol-theme/color/operator-name.ts
+++ b/src/mol-theme/color/operator-name.ts
@@ -85,6 +85,7 @@ export function OperatorNameColorTheme(ctx: ThemeDataContext, props: PD.Values<O
 
 export const OperatorNameColorThemeProvider: ColorTheme.Provider<OperatorNameColorThemeParams> = {
     label: 'Operator Name',
+    category: ColorTheme.Category.Symmetry,
     factory: OperatorNameColorTheme,
     getParams: getOperatorNameColorThemeParams,
     defaultValues: PD.getDefaultValues(OperatorNameColorThemeParams),
diff --git a/src/mol-theme/color/polymer-id.ts b/src/mol-theme/color/polymer-id.ts
index 46955e905c48bad6d951683869e8c9f3fb5f94ee..68649703483620535f88e05a03f19076ad7831d2 100644
--- a/src/mol-theme/color/polymer-id.ts
+++ b/src/mol-theme/color/polymer-id.ts
@@ -127,7 +127,8 @@ export function PolymerIdColorTheme(ctx: ThemeDataContext, props: PD.Values<Poly
 }
 
 export const PolymerIdColorThemeProvider: ColorTheme.Provider<PolymerIdColorThemeParams> = {
-    label: 'Polymer Id',
+    label: 'Polymer Chain Id',
+    category: ColorTheme.Category.Chain,
     factory: PolymerIdColorTheme,
     getParams: getPolymerIdColorThemeParams,
     defaultValues: PD.getDefaultValues(PolymerIdColorThemeParams),
diff --git a/src/mol-theme/color/polymer-index.ts b/src/mol-theme/color/polymer-index.ts
index 02bfa5b44266b20e431980f35b4d78545a5b5dbd..6d6c86dc9ae258c16509e6478d0b1a63ce6d974f 100644
--- a/src/mol-theme/color/polymer-index.ts
+++ b/src/mol-theme/color/polymer-index.ts
@@ -16,7 +16,7 @@ import { ColorLists } from '../../mol-util/color/lists';
 
 const DefaultList = 'dark-2'
 const DefaultColor = Color(0xCCCCCC)
-const Description = 'Gives every polymer a unique color based on the position (index) of the polymer in the list of polymers in the structure.'
+const Description = 'Gives every polymer chain instance a unique color based on the position (index) of the polymer in the list of polymers in the structure.'
 
 export const PolymerIndexColorThemeParams = {
     ...getPaletteParams({ type: 'set', setList: DefaultList }),
@@ -87,7 +87,8 @@ export function PolymerIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<P
 }
 
 export const PolymerIndexColorThemeProvider: ColorTheme.Provider<PolymerIndexColorThemeParams> = {
-    label: 'Polymer Index',
+    label: 'Polymer Chain Instance',
+    category: ColorTheme.Category.Chain,
     factory: PolymerIndexColorTheme,
     getParams: getPolymerIndexColorThemeParams,
     defaultValues: PD.getDefaultValues(PolymerIndexColorThemeParams),
diff --git a/src/mol-theme/color/residue-name.ts b/src/mol-theme/color/residue-name.ts
index 1703761c3b9252c2535b70a3e0f6d8ee50454916..741601e1cd2e6f37dd068d3d59857ac24b4e45de 100644
--- a/src/mol-theme/color/residue-name.ts
+++ b/src/mol-theme/color/residue-name.ts
@@ -74,22 +74,16 @@ export function getResidueNameColorThemeParams(ctx: ThemeDataContext) {
 }
 
 function getAtomicCompId(unit: Unit.Atomic, element: ElementIndex) {
-    const { modifiedResidues } = unit.model.properties
-    const compId = unit.model.atomicHierarchy.residues.label_comp_id.value(unit.residueIndex[element])
-    const parentId = modifiedResidues.parentId.get(compId)
-    return parentId === undefined ? compId : parentId
+    return unit.model.atomicHierarchy.residues.label_comp_id.value(unit.residueIndex[element])
 }
 
 function getCoarseCompId(unit: Unit.Spheres | Unit.Gaussians, element: ElementIndex) {
     const seqIdBegin = unit.coarseElements.seq_id_begin.value(element)
     const seqIdEnd = unit.coarseElements.seq_id_end.value(element)
     if (seqIdBegin === seqIdEnd) {
-        const { modifiedResidues } = unit.model.properties
         const entityKey = unit.coarseElements.entityKey[element]
         const seq = unit.model.sequence.byEntityKey[entityKey].sequence
-        let compId = seq.compId.value(seqIdBegin - 1) // 1-indexed
-        const parentId = modifiedResidues.parentId.get(compId)
-        return parentId === undefined ? compId : parentId
+        return seq.compId.value(seqIdBegin - 1) // 1-indexed
     }
 }
 
@@ -136,6 +130,7 @@ export function ResidueNameColorTheme(ctx: ThemeDataContext, props: PD.Values<Re
 
 export const ResidueNameColorThemeProvider: ColorTheme.Provider<ResidueNameColorThemeParams> = {
     label: 'Residue Name',
+    category: ColorTheme.Category.Residue,
     factory: ResidueNameColorTheme,
     getParams: getResidueNameColorThemeParams,
     defaultValues: PD.getDefaultValues(ResidueNameColorThemeParams),
diff --git a/src/mol-theme/color/secondary-structure.ts b/src/mol-theme/color/secondary-structure.ts
index 9eef3bb1c704290a78bd9cb9112104af87a1125a..12eaf2eb8df454b20d2b7b1d9cf1d7d2c6521da4 100644
--- a/src/mol-theme/color/secondary-structure.ts
+++ b/src/mol-theme/color/secondary-structure.ts
@@ -113,6 +113,7 @@ export function SecondaryStructureColorTheme(ctx: ThemeDataContext, props: PD.Va
 
 export const SecondaryStructureColorThemeProvider: ColorTheme.Provider<SecondaryStructureColorThemeParams> = {
     label: 'Secondary Structure',
+    category: ColorTheme.Category.Residue,
     factory: SecondaryStructureColorTheme,
     getParams: getSecondaryStructureColorThemeParams,
     defaultValues: PD.getDefaultValues(SecondaryStructureColorThemeParams),
diff --git a/src/mol-theme/color/sequence-id.ts b/src/mol-theme/color/sequence-id.ts
index 66457c0b576b579c692cae88ab9ccdc728aec05b..26881241a45d546ec50da3cba5c17b4cf6314487 100644
--- a/src/mol-theme/color/sequence-id.ts
+++ b/src/mol-theme/color/sequence-id.ts
@@ -101,6 +101,7 @@ export function SequenceIdColorTheme(ctx: ThemeDataContext, props: PD.Values<Seq
 
 export const SequenceIdColorThemeProvider: ColorTheme.Provider<SequenceIdColorThemeParams> = {
     label: 'Sequence Id',
+    category: ColorTheme.Category.Residue,
     factory: SequenceIdColorTheme,
     getParams: getSequenceIdColorThemeParams,
     defaultValues: PD.getDefaultValues(SequenceIdColorThemeParams),
diff --git a/src/mol-theme/color/shape-group.ts b/src/mol-theme/color/shape-group.ts
index 1c3166fafdadbefae4f83b7b9cd12d708a46ddf3..8a8232657dec24eac00254c08ba2bf2a458b4dc7 100644
--- a/src/mol-theme/color/shape-group.ts
+++ b/src/mol-theme/color/shape-group.ts
@@ -37,6 +37,7 @@ export function ShapeGroupColorTheme(ctx: ThemeDataContext, props: PD.Values<Sha
 
 export const ShapeGroupColorThemeProvider: ColorTheme.Provider<ShapeGroupColorThemeParams> = {
     label: 'Shape Group',
+    category: ColorTheme.Category.Misc,
     factory: ShapeGroupColorTheme,
     getParams: getShapeGroupColorThemeParams,
     defaultValues: PD.getDefaultValues(ShapeGroupColorThemeParams),
diff --git a/src/mol-theme/color/uncertainty.ts b/src/mol-theme/color/uncertainty.ts
index d7511cb0533f6554bf50d9d92797ef1b0df223f5..512dbea67781636037f14c4bbf34b9cf7202acf4 100644
--- a/src/mol-theme/color/uncertainty.ts
+++ b/src/mol-theme/color/uncertainty.ts
@@ -64,6 +64,7 @@ export function UncertaintyColorTheme(ctx: ThemeDataContext, props: PD.Values<Un
 
 export const UncertaintyColorThemeProvider: ColorTheme.Provider<UncertaintyColorThemeParams> = {
     label: 'Uncertainty/Disorder',
+    category: ColorTheme.Category.Atom,
     factory: UncertaintyColorTheme,
     getParams: getUncertaintyColorThemeParams,
     defaultValues: PD.getDefaultValues(UncertaintyColorThemeParams),
diff --git a/src/mol-theme/color/uniform.ts b/src/mol-theme/color/uniform.ts
index 6bc0e8bdc70be6be9be3c061eb5c7936ab8d270d..2fd01719375da61b3e1693a7ba213095e8895981 100644
--- a/src/mol-theme/color/uniform.ts
+++ b/src/mol-theme/color/uniform.ts
@@ -37,6 +37,7 @@ export function UniformColorTheme(ctx: ThemeDataContext, props: PD.Values<Unifor
 
 export const UniformColorThemeProvider: ColorTheme.Provider<UniformColorThemeParams> = {
     label: 'Uniform',
+    category: ColorTheme.Category.Misc,
     factory: UniformColorTheme,
     getParams: getUniformColorThemeParams,
     defaultValues: PD.getDefaultValues(UniformColorThemeParams),
diff --git a/src/mol-theme/color/unit-index.ts b/src/mol-theme/color/unit-index.ts
index bb6bb9c5bbc40dead1edf85da2ec110fb3011d13..3cc6cdcf26bbd93a71bda07611a6e1201cd149c7 100644
--- a/src/mol-theme/color/unit-index.ts
+++ b/src/mol-theme/color/unit-index.ts
@@ -16,7 +16,7 @@ import { ColorLists } from '../../mol-util/color/lists';
 
 const DefaultList = 'dark-2'
 const DefaultColor = Color(0xCCCCCC)
-const Description = 'Gives every unit (single chain or collection of single elements) a unique color based on the position (index) of the unit in the list of units in the structure.'
+const Description = 'Gives every chain instance (single chain or collection of single elements) a unique color based on the position (index) of the chain in the list of chains in the structure.'
 
 export const UnitIndexColorThemeParams = {
     ...getPaletteParams({ type: 'set', setList: DefaultList }),
@@ -72,7 +72,8 @@ export function UnitIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<Unit
 }
 
 export const UnitIndexColorThemeProvider: ColorTheme.Provider<UnitIndexColorThemeParams> = {
-    label: 'Unit Index',
+    label: 'Chain Instance',
+    category: ColorTheme.Category.Chain,
     factory: UnitIndexColorTheme,
     getParams: getUnitIndexColorThemeParams,
     defaultValues: PD.getDefaultValues(UnitIndexColorThemeParams),
diff --git a/src/mol-theme/size.ts b/src/mol-theme/size.ts
index 71c79b8d22d463ca3b3efa0779c41e5b9d77f227..2283474f79175887aa15b56780d5c71b6c01fffa 100644
--- a/src/mol-theme/size.ts
+++ b/src/mol-theme/size.ts
@@ -32,7 +32,7 @@ namespace SizeTheme {
     }
 
     export interface Provider<P extends PD.Params> extends ThemeProvider<SizeTheme<P>, P> { }
-    export const EmptyProvider: Provider<{}> = { label: '', factory: EmptyFactory, getParams: () => ({}), defaultValues: {}, isApplicable: () => true }
+    export const EmptyProvider: Provider<{}> = { label: '', category: '', factory: EmptyFactory, getParams: () => ({}), defaultValues: {}, isApplicable: () => true }
 
     export type Registry = ThemeRegistry<SizeTheme<any>>
     export function createRegistry() {
diff --git a/src/mol-theme/size/physical.ts b/src/mol-theme/size/physical.ts
index 4456ad2091c98280488f9c1dd34f1e42784efc6a..8622967d19f8e774b71b9446d580530e5f9ce01d 100644
--- a/src/mol-theme/size/physical.ts
+++ b/src/mol-theme/size/physical.ts
@@ -58,6 +58,7 @@ export function PhysicalSizeTheme(ctx: ThemeDataContext, props: PD.Values<Physic
 
 export const PhysicalSizeThemeProvider: SizeTheme.Provider<PhysicalSizeThemeParams> = {
     label: 'Physical',
+    category: '',
     factory: PhysicalSizeTheme,
     getParams: getPhysicalSizeThemeParams,
     defaultValues: PD.getDefaultValues(PhysicalSizeThemeParams),
diff --git a/src/mol-theme/size/shape-group.ts b/src/mol-theme/size/shape-group.ts
index 2e2f867d55c5910a8ca844a4cb42215c77607412..b7eaa993fe5d1afcf5af100b6c4fb2ce215f2c26 100644
--- a/src/mol-theme/size/shape-group.ts
+++ b/src/mol-theme/size/shape-group.ts
@@ -36,6 +36,7 @@ export function ShapeGroupSizeTheme(ctx: ThemeDataContext, props: PD.Values<Shap
 
 export const ShapeGroupSizeThemeProvider: SizeTheme.Provider<ShapeGroupSizeThemeParams> = {
     label: 'Shape Group',
+    category: '',    
     factory: ShapeGroupSizeTheme,
     getParams: getShapeGroupSizeThemeParams,
     defaultValues: PD.getDefaultValues(ShapeGroupSizeThemeParams),
diff --git a/src/mol-theme/size/uncertainty.ts b/src/mol-theme/size/uncertainty.ts
index 5f9da065b4b8331ac620db16e6e4b806446ac176..bbeec077745086a04cdb72d76cd9ef911cd0595f 100644
--- a/src/mol-theme/size/uncertainty.ts
+++ b/src/mol-theme/size/uncertainty.ts
@@ -54,6 +54,7 @@ export function UncertaintySizeTheme(ctx: ThemeDataContext, props: PD.Values<Unc
 
 export const UncertaintySizeThemeProvider: SizeTheme.Provider<UncertaintySizeThemeParams> = {
     label: 'Uncertainty/Disorder',
+    category: '',
     factory: UncertaintySizeTheme,
     getParams: getUncertaintySizeThemeParams,
     defaultValues: PD.getDefaultValues(UncertaintySizeThemeParams),
diff --git a/src/mol-theme/size/uniform.ts b/src/mol-theme/size/uniform.ts
index 4d82bef1048e7b01b74c711df38221f3fd3aafdd..32b2718744f145c5ec3540eefb8a7326f46f7d0a 100644
--- a/src/mol-theme/size/uniform.ts
+++ b/src/mol-theme/size/uniform.ts
@@ -32,6 +32,7 @@ export function UniformSizeTheme(ctx: ThemeDataContext, props: PD.Values<Uniform
 
 export const UniformSizeThemeProvider: SizeTheme.Provider<UniformSizeThemeParams> = {
     label: 'Uniform',
+    category: '',
     factory: UniformSizeTheme,
     getParams: getUniformSizeThemeParams,
     defaultValues: PD.getDefaultValues(UniformSizeThemeParams),
diff --git a/src/mol-theme/theme.ts b/src/mol-theme/theme.ts
index ce5350a5a696a2b2315415576755f1556667dea2..e4d038f175147906882d58b5df7e6c6f2e15f9da 100644
--- a/src/mol-theme/theme.ts
+++ b/src/mol-theme/theme.ts
@@ -61,6 +61,7 @@ namespace Theme {
 
 export interface ThemeProvider<T extends ColorTheme<P> | SizeTheme<P>, P extends PD.Params> {
     readonly label: string
+    readonly category: string
     readonly factory: (ctx: ThemeDataContext, props: PD.Values<P>) => T
     readonly getParams: (ctx: ThemeDataContext) => P
     readonly defaultValues: PD.Values<P>
@@ -69,7 +70,7 @@ export interface ThemeProvider<T extends ColorTheme<P> | SizeTheme<P>, P extends
 }
 
 function getTypes(list: { name: string, provider: ThemeProvider<any, any> }[]) {
-    return list.map(e => [e.name, e.provider.label] as [string, string]);
+    return list.map(e => [e.name, e.provider.label, e.provider.category] as [string, string, string]);
 }
 
 export class ThemeRegistry<T extends ColorTheme<any> | SizeTheme<any>> {
@@ -79,16 +80,26 @@ export class ThemeRegistry<T extends ColorTheme<any> | SizeTheme<any>> {
 
     get default() { return this._list[0] }
     get list() { return this._list }
-    get types(): [string, string][] { return getTypes(this._list) }
+    get types(): [string, string, string][] { return getTypes(this._list) }
 
     constructor(builtInThemes: { [k: string]: ThemeProvider<T, any> }, private emptyProvider: ThemeProvider<T, any>) {
         Object.keys(builtInThemes).forEach(name => this.add(name, builtInThemes[name]))
     }
 
+    private sort() {
+        this._list.sort((a, b) => {
+            if (a.provider.category === b.provider.category) {
+                return a.provider.label < b.provider.label ? -1 : a.provider.label > b.provider.label ? 1 : 0;
+            }
+            return a.provider.category < b.provider.category ? -1 : 1;
+        });
+    }
+
     add<P extends PD.Params>(name: string, provider: ThemeProvider<T, P>) {
         this._list.push({ name, provider })
         this._map.set(name, provider)
         this._name.set(provider, name)
+        this.sort();
     }
 
     remove(name: string) {
diff --git a/src/mol-util/param-definition.ts b/src/mol-util/param-definition.ts
index edcf9300c4635fb1ac00869978d8f9457f58906c..c835570ab397c7db04c08804bed2277b79b6d756 100644
--- a/src/mol-util/param-definition.ts
+++ b/src/mol-util/param-definition.ts
@@ -65,9 +65,9 @@ export namespace ParamDefinition {
     export interface Select<T extends string | number> extends Base<T> {
         type: 'select'
         /** array of (value, label) tuples */
-        options: readonly (readonly [T, string])[]
+        options: readonly (readonly [T, string] | readonly [T, string, string])[]
     }
-    export function Select<T extends string | number>(defaultValue: T, options: readonly (readonly [T, string])[], info?: Info): Select<T> {
+    export function Select<T extends string | number>(defaultValue: T, options: readonly (readonly [T, string] | readonly [T, string, string])[], info?: Info): Select<T> {
         return setInfo<Select<T>>({ type: 'select', defaultValue: checkDefaultKey(defaultValue, options), options }, info)
     }
 
@@ -111,11 +111,11 @@ export namespace ParamDefinition {
         return setInfo<Color>({ type: 'color', defaultValue }, info)
     }
 
-    export interface Vec3 extends Base<Vec3Data> {
+    export interface Vec3 extends Base<Vec3Data>, Range {
         type: 'vec3'
     }
-    export function Vec3(defaultValue: Vec3Data, info?: Info): Vec3 {
-        return setInfo<Vec3>({ type: 'vec3', defaultValue }, info)
+    export function Vec3(defaultValue: Vec3Data, range?: { min?: number, max?: number, step?: number }, info?: Info): Vec3 {
+        return setInfo<Vec3>(setRange({ type: 'vec3', defaultValue }, range), info)
     }
 
     export interface FileParam extends Base<File> {
@@ -149,7 +149,7 @@ export namespace ParamDefinition {
          */
         step?: number
     }
-    function setRange<T extends Numeric | Interval>(p: T, range?: { min?: number, max?: number, step?: number }) {
+    function setRange<T extends Numeric | Interval | Vec3>(p: T, range?: { min?: number, max?: number, step?: number }) {
         if (!range) return p;
         if (typeof range.min !== 'undefined') p.min = range.min;
         if (typeof range.max !== 'undefined') p.max = range.max;
@@ -201,7 +201,7 @@ export namespace ParamDefinition {
         select: Select<string>,
         map(name: string): Any
     }
-    export function Mapped<T>(defaultKey: string, names: [string, string][], map: (name: string) => Any, info?: Info): Mapped<NamedParams<T>> {
+    export function Mapped<T>(defaultKey: string, names: ([string, string] | [string, string, string])[], map: (name: string) => Any, info?: Info): Mapped<NamedParams<T>> {
         const name = checkDefaultKey(defaultKey, names);
         return setInfo<Mapped<NamedParams<T>>>({
             type: 'mapped',
@@ -406,7 +406,7 @@ export namespace ParamDefinition {
         return ret;
     }
 
-    function checkDefaultKey<T>(k: T, options: readonly (readonly [T, string])[]) {
+    function checkDefaultKey<T>(k: T, options: readonly (readonly [T, string] | readonly [T, string, string])[]) {
         for (const o of options) {
             if (o[0] === k) return k;
         }
diff --git a/src/mol-util/param-mapping.ts b/src/mol-util/param-mapping.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e85731ffa8f14e9ab2f8b69dcf46c57264dd77c7
--- /dev/null
+++ b/src/mol-util/param-mapping.ts
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { ParamDefinition as PD } from './param-definition';
+import produce from 'immer'
+import { Mutable } from './type-helpers';
+
+export interface ParamMapping<S, T, Ctx> {
+    params(ctx: Ctx): PD.For<S>,
+    getTarget(ctx: Ctx): T,
+    getValues(t: T, ctx: Ctx): S,
+    update(s: S, ctx: Ctx): T,
+    apply(t: T, ctx: Ctx): void | Promise<void>
+}
+
+export function ParamMapping<S, T, Ctx>(def: {
+    params: ((ctx: Ctx) => PD.For<S>) | PD.For<S>,
+    target(ctx: Ctx): T
+}): (options: {
+    values(t: T, ctx: Ctx): S,
+    update(s: S, t: Mutable<T>, ctx: Ctx): void,
+    apply?(t: T, ctx: Ctx): void | Promise<void>
+}) => ParamMapping<S, T, Ctx> {
+    return ({ values, update, apply }) => ({
+        params: typeof def.params === 'function' ? def.params as any : ctx => def.params,
+        getTarget: def.target,
+        getValues: values,
+        update(s, ctx) {
+            const t = def.target(ctx);
+            return produce(t, t1 => update(s, t1 as any, ctx));
+        },
+        apply: apply ? apply : () => { }
+    });
+}
\ No newline at end of file
diff --git a/src/mol-util/set.ts b/src/mol-util/set.ts
index af02d56a3cab789b73612da9c24a48d43c4c461f..9e1f71738511e4ef34547c9aae44f375f635859b 100644
--- a/src/mol-util/set.ts
+++ b/src/mol-util/set.ts
@@ -4,9 +4,15 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
+import { iterableToArray } from '../mol-data/util/array';
+
 // TODO use set@@iterator when targeting es6
 
 export namespace SetUtils {
+    export function toArray<T>(set: ReadonlySet<T>) {
+        return iterableToArray(set.values())
+    }
+
     /** Test if set a contains all elements of set b. */
     export function isSuperset<T>(setA: ReadonlySet<T>, setB: ReadonlySet<T>) {
         let flag = true
diff --git a/webpack.config.js b/webpack.config.js
index f33a385636c5aff1423777bb51873a7d60824ddb..9e9aceeaccfddf66f9691890c05f5a854fabdfb7 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -46,7 +46,8 @@ const sharedConfig = {
             'node_modules',
             path.resolve(__dirname, 'lib/')
         ],
-    }
+    },
+    devtool: ''
 }