diff --git a/package-lock.json b/package-lock.json index 2b72f7489f1079bc12ee416df0d1e8ccb8e37d0b..2cecc420bbb9f81995659697b963ec4668a7aaff 100644 Binary files a/package-lock.json and b/package-lock.json differ diff --git a/package.json b/package.json index 3c72052a1fe35f1638fae0ee923a33acdda51e80..2e211903084148e38a28f2c317b4dd7699b56511 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "molstar", - "version": "0.2.2", + "version": "0.2.3", "description": "A comprehensive macromolecular library.", "homepage": "https://github.com/molstar/molstar#readme", "repository": { diff --git a/src/examples/proteopedia-wrapper/changelog.md b/src/examples/proteopedia-wrapper/changelog.md index 9680f701e7da996b4d28c9627f87833d9571539d..b9d8bd109b9c5e063795fe46f0cc3d03297e06aa 100644 --- a/src/examples/proteopedia-wrapper/changelog.md +++ b/src/examples/proteopedia-wrapper/changelog.md @@ -1,3 +1,8 @@ +== v3.2 == + +* Fixed assembly loading. +* Better HET group focus. + == v3.0 == * Fixed initial camera zoom. diff --git a/src/examples/proteopedia-wrapper/helpers.ts b/src/examples/proteopedia-wrapper/helpers.ts index 7d1360c3e31e095ea745b9afb91d16e54cb21131..d8d4f26101b9635cf14936e4657c71565780cc30 100644 --- a/src/examples/proteopedia-wrapper/helpers.ts +++ b/src/examples/proteopedia-wrapper/helpers.ts @@ -115,5 +115,6 @@ export enum StateElements { Water = 'water', WaterVisual = 'water-visual', - HetGroupFocus = 'het-group-focus' + HetGroupFocus = 'het-group-focus', + HetGroupFocusGroup = 'het-group-focus-group' } \ No newline at end of file diff --git a/src/examples/proteopedia-wrapper/index.ts b/src/examples/proteopedia-wrapper/index.ts index d444fed9d4c138a0488ff63cb1d0a8cd19dc1b15..956ae98c1ba12ae93b9f97dd761b020355572bdb 100644 --- a/src/examples/proteopedia-wrapper/index.ts +++ b/src/examples/proteopedia-wrapper/index.ts @@ -36,7 +36,7 @@ require('../../mol-plugin/skin/light.scss') class MolStarProteopediaWrapper { static VERSION_MAJOR = 3; - static VERSION_MINOR = 1; + static VERSION_MINOR = 2; private _ev = RxEventHelper.create(); @@ -81,7 +81,7 @@ class MolStarProteopediaWrapper { return b.apply(StateTransforms.Data.Download, { url, isBinary: false }) } - private model(b: StateBuilder.To<PSO.Data.Binary | PSO.Data.String>, format: SupportedFormats, assemblyId: string) { + private model(b: StateBuilder.To<PSO.Data.Binary | PSO.Data.String>, format: SupportedFormats) { const parsed = format === 'cif' ? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif) : b.apply(StateTransforms.Model.TrajectoryFromPDB); @@ -190,7 +190,7 @@ class MolStarProteopediaWrapper { } private loadedParams: LoadParams = { url: '', format: 'cif', assemblyId: '' }; - async load({ url, format = 'cif', assemblyId = '', representationStyle }: LoadParams) { + async load({ url, format = 'cif', assemblyId = 'deposited', representationStyle }: LoadParams) { let loadType: 'full' | 'update' = 'full'; const state = this.plugin.state.dataState; @@ -203,14 +203,17 @@ class MolStarProteopediaWrapper { if (loadType === 'full') { await PluginCommands.State.RemoveObject.dispatch(this.plugin, { state, ref: state.tree.root.ref }); - const modelTree = this.model(this.download(state.build().toRoot(), url), format, assemblyId); + const modelTree = this.model(this.download(state.build().toRoot(), url), format); await this.applyState(modelTree); const info = await this.doInfo(true); - const structureTree = this.structure((assemblyId === 'preferred' && info && info.preferredAssemblyId) || assemblyId); + const asmId = (assemblyId === 'preferred' && info && info.preferredAssemblyId) || assemblyId; + const structureTree = this.structure(asmId); await this.applyState(structureTree); } else { const tree = state.build(); - tree.to(StateElements.Assembly).update(StateTransforms.Model.StructureAssemblyFromModel, p => ({ ...p, id: assemblyId || 'deposited' })); + 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 })); await this.applyState(tree); } @@ -295,30 +298,30 @@ class MolStarProteopediaWrapper { PluginCommands.State.Update.dispatch(this.plugin, { state: this.state, tree: update }); PluginCommands.Camera.Reset.dispatch(this.plugin, { }); }, - focusFirst: async (resn: string) => { + focusFirst: async (compId: string) => { if (!this.state.transforms.has(StateElements.Assembly)) return; + await PluginCommands.Camera.Reset.dispatch(this.plugin, { }); // const asm = (this.state.select(StateElements.Assembly)[0].obj as PluginStateObject.Molecule.Structure).data; const update = this.state.build(); - update.delete(StateElements.HetGroupFocus); + update.delete(StateElements.HetGroupFocusGroup); - const surroundings = MS.struct.modifier.includeSurroundings({ - 0: MS.struct.filter.first([ - MS.struct.generator.atomGroups({ - 'residue-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.label_comp_id(), resn]), - 'group-by': MS.struct.atomProperty.macromolecular.residueKey() - }) - ]), - radius: 5, - 'as-whole-residues': true - }); + const core = MS.struct.filter.first([ + MS.struct.generator.atomGroups({ + 'residue-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.label_comp_id(), compId]), + 'group-by': MS.core.str.concat([MS.struct.atomProperty.core.operatorName(), MS.struct.atomProperty.macromolecular.residueKey()]) + }) + ]); + const surroundings = MS.struct.modifier.includeSurroundings({ 0: core, radius: 5, 'as-whole-residues': true }); - const sel = update.to(StateElements.Assembly) - .apply(StateTransforms.Model.StructureSelection, { label: resn, query: surroundings }, { ref: StateElements.HetGroupFocus }); + const group = update.to(StateElements.Assembly).group(StateTransforms.Misc.CreateGroup, { label: compId }, { ref: StateElements.HetGroupFocusGroup }); - sel.apply(StateTransforms.Representation.StructureRepresentation3D, this.createSurVisualParams()); + group.apply(StateTransforms.Model.StructureSelection, { label: 'Core', query: core }, { ref: StateElements.HetGroupFocus }) + .apply(StateTransforms.Representation.StructureRepresentation3D, this.createCoreVisualParams()); + group.apply(StateTransforms.Model.StructureSelection, { label: 'Surroundings', query: surroundings }) + .apply(StateTransforms.Representation.StructureRepresentation3D, this.createSurVisualParams()); // sel.apply(StateTransforms.Representation.StructureLabels3D, { // target: { name: 'residues', params: { } }, // options: { @@ -338,7 +341,7 @@ class MolStarProteopediaWrapper { // const position = Vec3.sub(Vec3.zero(), sphere.center, asmCenter); // Vec3.normalize(position, position); // Vec3.scaleAndAdd(position, sphere.center, position, sphere.radius); - const snapshot = this.plugin.canvas3d.camera.getFocus(sphere.center, 0.75 * sphere.radius); + const snapshot = this.plugin.canvas3d.camera.getFocus(sphere.center, Math.max(sphere.radius, 5)); PluginCommands.Camera.SetSnapshot.dispatch(this.plugin, { snapshot, durationMs: 250 }); } } @@ -352,6 +355,15 @@ class MolStarProteopediaWrapper { }); } + private createCoreVisualParams() { + const asm = this.state.select(StateElements.Assembly)[0].obj as PluginStateObject.Molecule.Structure; + return StructureRepresentation3DHelpers.createParams(this.plugin, asm.data, { + repr: BuiltInStructureRepresentations['ball-and-stick'], + // color: [BuiltInColorThemes.uniform, () => ({ value: ColorNames.gray })], + // size: [BuiltInSizeThemes.uniform, () => ({ value: 0.33 } )] + }); + } + snapshot = { get: () => { return this.plugin.state.getSnapshot(); diff --git a/src/mol-io/writer/_spec/cif.spec.ts b/src/mol-io/writer/_spec/cif.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..fba3f3c51ce81a7b14f07a9d420afe878fda4568 --- /dev/null +++ b/src/mol-io/writer/_spec/cif.spec.ts @@ -0,0 +1,183 @@ +import * as Data from '../../reader/cif/data-model' +import { CifWriter } from '../cif'; +import decodeMsgPack from '../../common/msgpack/decode' +import { EncodedFile, EncodedCategory } from '../../common/binary-cif'; +import Field from '../../reader/cif/binary/field'; +import * as C from '../cif/encoder'; + +const cartn_x = Data.CifField.ofNumbers([1.001, 1.002, 1.003, 1.004, 1.005, 1.006, 1.007, 1.008, 1.009]); +const cartn_y = Data.CifField.ofNumbers([-3.0, -2.666, -2.3333, -2.0, -1.666, -1.333, -1.0, -0.666, -0.333]); +const cartn_z = Data.CifField.ofNumbers([1, 2, 3, 4, 5, 6, 7, 8, 9].map(i => Math.sqrt(i))); +const label_seq_id = Data.CifField.ofNumbers([1, 2, 3, 6, 11, 23, 47, 106, 235]); +const atom_site = Data.CifCategory.ofFields('atom_site', { 'Cartn_x': cartn_x, 'Cartn_y': cartn_y, 'Cartn_z': cartn_z, 'label_seq_id': label_seq_id }); +const field1 = Data.CifField.ofNumbers([1, 2, 3, 6, 11, 23, 47, 106, 235]); +const field2 = Data.CifField.ofNumbers([-1, -2, -3, -6, -11, -23, -47, -106, -235]); +const other_fields = Data.CifCategory.ofFields('other_fields', { 'field1': field1, 'field2': field2 }); + +const encoding_aware_encoder = CifWriter.createEncoder({ + binary: true, + binaryAutoClassifyEncoding: true, + binaryEncodingPovider: CifWriter.createEncodingProviderFromJsonConfig([ + { + 'categoryName': 'atom_site', + 'columnName': 'Cartn_y', + 'encoding': 'rle', + 'precision': 0 + }, + { + 'categoryName': 'atom_site', + 'columnName': 'Cartn_z', + 'encoding': 'delta', + 'precision': 1 + }, + { + 'categoryName': 'atom_site', + 'columnName': 'label_seq_id', + 'encoding': 'delta-rle' + } + ]) +}); + +describe('encoding-config', () => { + const decoded = process(encoding_aware_encoder); + + const decoded_atom_site = decoded.blocks[0].categories['atom_site']; + const decoded_cartn_x = decoded_atom_site.getField('Cartn_x')!; + const decoded_cartn_y = decoded_atom_site.getField('Cartn_y')!; + const decoded_cartn_z = decoded_atom_site.getField('Cartn_z')!; + const decoded_label_seq_id = decoded_atom_site.getField('label_seq_id')!; + + const delta = 0.001; + function assert(e: ArrayLike<number>, a: ArrayLike<number>) { + expect(e.length).toBe(a.length); + for (let i = 0; i < e.length; i++) { + expect(Math.abs(e[i] - a[i])).toBeLessThan(delta); + } + } + + function join(field: Data.CifField) { + return field.binaryEncoding!.map(e => e.kind).join(); + } + + it('strategy', () => { + expect(join(decoded_cartn_x)).toBe('FixedPoint,Delta,IntegerPacking,ByteArray'); + expect(join(decoded_cartn_y)).toBe('FixedPoint,RunLength,IntegerPacking,ByteArray'); + expect(join(decoded_cartn_z)).toBe('FixedPoint,Delta,IntegerPacking,ByteArray'); + expect(join(decoded_label_seq_id)).toBe('Delta,RunLength,IntegerPacking,ByteArray'); + }); + + it('precision', () => { + assert(decoded_cartn_x.toFloatArray(), cartn_x.toFloatArray()); + assert(decoded_cartn_y.toFloatArray(), cartn_y.toFloatArray().map(d => Math.round(d))); + assert(decoded_cartn_z.toFloatArray(), cartn_z.toFloatArray().map(d => Math.round(d * 10) / 10)); + assert(decoded_label_seq_id.toIntArray(), label_seq_id.toIntArray()); + }); +}); + +const filter_aware_encoder1 = CifWriter.createEncoder({ + binary: true, + binaryAutoClassifyEncoding: true +}); +filter_aware_encoder1.setFilter(C.Category.filterOf('atom_site\n' + +'\n' + +'atom_site.Cartn_x\n' + +'atom_site.Cartn_y\n')); + +const filter_aware_encoder2 = CifWriter.createEncoder({ + binary: true +}); +filter_aware_encoder2.setFilter(C.Category.filterOf('!atom_site\n' + +'\n' + +'!other_fields.field2\n')); + +describe('filtering-config', () => { + const decoded1 = process(filter_aware_encoder1); + + const atom_site1 = decoded1.blocks[0].categories['atom_site']; + const cartn_x1 = atom_site1.getField('Cartn_x'); + const cartn_y1 = atom_site1.getField('Cartn_y'); + const cartn_z1 = atom_site1.getField('Cartn_z'); + const label_seq_id1 = atom_site1.getField('label_seq_id'); + const fields1 = decoded1.blocks[0].categories['other_fields']; + + it('whitelist-filtering', () => { + expect(atom_site1).toBeDefined(); + expect(cartn_x1).toBeDefined(); + expect(cartn_y1).toBeDefined(); + expect(cartn_z1).toBeUndefined(); + expect(label_seq_id1).toBeUndefined(); + expect(fields1).toBeUndefined(); + }); + + const decoded2 = process(filter_aware_encoder2); + + const atom_site2 = decoded2.blocks[0].categories['atom_site']; + const fields2 = decoded2.blocks[0].categories['other_fields']; + const field12 = fields2.getField('field1'); + const field22 = fields2.getField('field2'); + + it('blacklist-filtering', () => { + expect(atom_site2).toBeUndefined(); + expect(fields2).toBeDefined(); + expect(field12).toBeDefined(); + expect(field22).toBeUndefined(); + }); +}); + +function process(encoder: C.Encoder) { + encoder.startDataBlock('test'); + + for (const cat of [atom_site, other_fields]) { + const fields: CifWriter.Field[] = []; + for (const f of cat.fieldNames) { + fields.push(wrap(f, cat.getField(f)!)) + } + encoder.writeCategory(getCategoryInstanceProvider(cat, fields)); + } + + const encoded = encoder.getData() as Uint8Array; + + const unpacked = decodeMsgPack(encoded) as EncodedFile; + return Data.CifFile(unpacked.dataBlocks.map(block => { + const cats = Object.create(null); + for (const cat of block.categories) cats[cat.name.substr(1)] = Category(cat); + return Data.CifBlock(block.categories.map(c => c.name.substr(1)), cats, block.header); + })); +} + + +function getCategoryInstanceProvider(cat: Data.CifCategory, fields: CifWriter.Field[]): CifWriter.Category { + return { + name: cat.name, + instance: () => CifWriter.categoryInstance(fields, { data: cat, rowCount: cat.rowCount }) + }; +} + +function wrap(name: string, field: Data.CifField): CifWriter.Field { + const type = Data.getCifFieldType(field); + if (type['@type'] === 'str') { + return { name, type: CifWriter.Field.Type.Str, value: field.str, valueKind: field.valueKind }; + } else if (type['@type'] === 'float') { + return { name, type: CifWriter.Field.Type.Float, value: field.float, valueKind: field.valueKind }; + } else { + return { name, type: CifWriter.Field.Type.Int, value: field.int, valueKind: field.valueKind }; + } +} + +function Category(data: EncodedCategory): Data.CifCategory { + const map = Object.create(null); + const cache = Object.create(null); + for (const col of data.columns) map[col.name] = col; + return { + rowCount: data.rowCount, + name: data.name.substr(1), + fieldNames: data.columns.map(c => c.name), + getField(name) { + const col = map[name]; + if (!col) return void 0; + if (!!cache[name]) return cache[name]; + cache[name] = Field(col); + return cache[name]; + } + } +} \ No newline at end of file diff --git a/src/mol-io/writer/cif.ts b/src/mol-io/writer/cif.ts index c191d8a4bdeca25d9c377abe354980931bcad2f3..76298baf837774d20b1f727053960da10d2247fc 100644 --- a/src/mol-io/writer/cif.ts +++ b/src/mol-io/writer/cif.ts @@ -53,5 +53,64 @@ export namespace CifWriter { return ff && ff.binaryEncoding ? ArrayEncoder.fromEncoding(ff.binaryEncoding) : void 0; } } + }; + + export function createEncodingProviderFromJsonConfig(hints: EncodingStrategyHint[]): EncodingProvider { + return { + get(c, f) { + for (let i = 0; i < hints.length; i++) { + const hint = hints[i]; + if (hint.categoryName === c && hint.columnName === f) { + return resolveEncoding(hint); + } + } + } + } + } + + function resolveEncoding(hint: EncodingStrategyHint): ArrayEncoder { + const precision: number | undefined = hint.precision; + if (precision !== void 0) { + const multiplier = Math.pow(10, precision); + const fixedPoint = E.by(E.fixedPoint(multiplier)); + switch (hint.encoding) { + case 'pack': + return fixedPoint.and(E.integerPacking); + case 'rle': + return fixedPoint.and(E.runLength).and(E.integerPacking); + case 'delta': + return fixedPoint.and(E.delta).and(E.integerPacking); + case 'delta-rle': + return fixedPoint.and(E.delta).and(E.runLength).and(E.integerPacking); + }; + } else { + switch (hint.encoding) { + case 'pack': + return E.by(E.integerPacking); + case 'rle': + return E.by(E.runLength).and(E.integerPacking); + case 'delta': + return E.by(E.delta).and(E.integerPacking); + case 'delta-rle': + return E.by(E.delta).and(E.runLength).and(E.integerPacking); + } + } + throw new Error('cannot be reached'); } -} \ No newline at end of file +} + +/** + * Defines the information needed to encode certain fields: category and column name as well as encoding tag, precision is optional and identifies float columns. + */ +export interface EncodingStrategyHint { + categoryName: string, + columnName: string, + // TODO would be nice to infer strategy and precision if needed + encoding: EncodingType, + /** + * number of decimal places to keep - must be specified to float columns + */ + precision?: number +} + +type EncodingType = 'pack' | 'rle' | 'delta' | 'delta-rle' \ No newline at end of file diff --git a/src/mol-io/writer/cif/encoder.ts b/src/mol-io/writer/cif/encoder.ts index f0d95d18b7543d92f48c50d7f5f7897ed371befa..9c0b8a743f3377edd90527b13977b95165fbfe03 100644 --- a/src/mol-io/writer/cif/encoder.ts +++ b/src/mol-io/writer/cif/encoder.ts @@ -132,6 +132,60 @@ export namespace Category { includeField(categoryName: string, fieldName: string): boolean, } + export function filterOf(directives: string): Filter { + const cat_whitelist: string[] = []; + const cat_blacklist: string[] = []; + const field_whitelist: string[] = []; + const field_blacklist: string[] = []; + + for (let d of directives.split(/[\r\n]+/)) { + d = d.trim(); + // allow for empty lines in config + if (d.length === 0) continue; + // let ! denote blacklisted entries + const blacklist = /^!/.test(d); + if (blacklist) d = d.substr(1); + const split = d.split(/\./); + const field = split[1]; + const list = blacklist ? (field ? field_blacklist : cat_blacklist) : (field ? field_whitelist : cat_whitelist); + + list[list.length] = d; + + // ensure categories are aware about whitelisted columns + if (field && !cat_whitelist.includes(split[0])) { + cat_whitelist[cat_whitelist.length] = split[0]; + } + } + + const wlcatcol = field_whitelist.map(it => it.split('.')[0]); + // blacklist has higher priority + return { + includeCategory(cat) { + // block if category in black + if (cat_blacklist.includes(cat)) { + return false; + } else { + // if there is a whitelist, the category has to be explicitly allowed + return cat_whitelist.length <= 0 || + // otherwise include if whitelist contains category + cat_whitelist.indexOf(cat) !== -1; + } + }, + includeField(cat, field) { + // column names are assumed to follow the pattern 'category_name.column_name' + const full = cat + '.' + field; + if (field_blacklist.includes(full)) { + return false; + } else { + // if for this category no whitelist entries exist + return !wlcatcol.includes(cat) || + // otherwise must be specifically allowed + field_whitelist.includes(full); + } + } + } + } + export const DefaultFilter: Filter = { includeCategory(cat) { return true; }, includeField(cat, field) { return true; }