Skip to content
Snippets Groups Projects
Commit 393c8ea7 authored by Alexander Rose's avatar Alexander Rose
Browse files

first pass at adding rcsb assembly symmetry plugin

parent 61f61358
No related branches found
No related tags found
No related merge requests found
...@@ -117,7 +117,6 @@ ...@@ -117,7 +117,6 @@
"compression": "^1.7.3", "compression": "^1.7.3",
"express": "^4.16.4", "express": "^4.16.4",
"graphql": "^14.0.2", "graphql": "^14.0.2",
"graphql-request": "^1.8.2",
"immutable": "^3.8.2", "immutable": "^3.8.2",
"node-fetch": "^2.3.0", "node-fetch": "^2.3.0",
"react": "^16.6.3", "react": "^16.6.3",
......
...@@ -4,8 +4,6 @@ ...@@ -4,8 +4,6 @@
* @author Alexander Rose <alexander.rose@weirdbyte.de> * @author Alexander Rose <alexander.rose@weirdbyte.de>
*/ */
import { GraphQLClient } from 'graphql-request'
import { AssemblySymmetry as AssemblySymmetryGraphQL } from './graphql/types'; import { AssemblySymmetry as AssemblySymmetryGraphQL } from './graphql/types';
import query from './graphql/symmetry.gql'; import query from './graphql/symmetry.gql';
...@@ -18,6 +16,9 @@ import { CifExportContext } from 'mol-model/structure/export/mmcif'; ...@@ -18,6 +16,9 @@ import { CifExportContext } from 'mol-model/structure/export/mmcif';
import { toTable } from 'mol-io/reader/cif/schema'; import { toTable } from 'mol-io/reader/cif/schema';
import { CifCategory } from 'mol-io/reader/cif'; import { CifCategory } from 'mol-io/reader/cif';
import { PropertyWrapper } from 'mol-model-props/common/wrapper'; import { PropertyWrapper } from 'mol-model-props/common/wrapper';
import { Task, RuntimeContext } from 'mol-task';
import { GraphQLClient } from 'mol-util/graphql-client';
import { ajaxGet } from 'mol-util/data-source';
const { str, int, float, Aliased, Vector, List } = Column.Schema; const { str, int, float, Aliased, Vector, List } = Column.Schema;
...@@ -152,8 +153,6 @@ const _Descriptor: ModelPropertyDescriptor = { ...@@ -152,8 +153,6 @@ const _Descriptor: ModelPropertyDescriptor = {
} }
} }
const client = new GraphQLClient('http://rest-experimental.rcsb.org/graphql')
export interface AssemblySymmetry { export interface AssemblySymmetry {
db: AssemblySymmetry.Database db: AssemblySymmetry.Database
getSymmetries(assemblyId: string): Table<AssemblySymmetry.Schema['rcsb_assembly_symmetry']> getSymmetries(assemblyId: string): Table<AssemblySymmetry.Schema['rcsb_assembly_symmetry']>
...@@ -177,7 +176,10 @@ export function AssemblySymmetry(db: AssemblySymmetry.Database): AssemblySymmetr ...@@ -177,7 +176,10 @@ export function AssemblySymmetry(db: AssemblySymmetry.Database): AssemblySymmetr
} }
} }
const Client = new GraphQLClient(AssemblySymmetry.GraphQLEndpointURL, (url: string, type: 'string' | 'binary', body?: string) => ajaxGet({ url, type, body }) )
export namespace AssemblySymmetry { export namespace AssemblySymmetry {
export const GraphQLEndpointURL = 'http://rest-experimental.rcsb.org/graphql'
export const Schema = { export const Schema = {
rcsb_assembly_symmetry_info: { rcsb_assembly_symmetry_info: {
updated_datetime_utc: Column.Schema.str updated_datetime_utc: Column.Schema.str
...@@ -247,7 +249,7 @@ export namespace AssemblySymmetry { ...@@ -247,7 +249,7 @@ export namespace AssemblySymmetry {
export const Descriptor = _Descriptor; export const Descriptor = _Descriptor;
export async function attachFromCifOrAPI(model: Model) { export async function attachFromCifOrAPI(model: Model, client: GraphQLClient = Client, ctx?: RuntimeContext) {
if (model.customProperties.has(Descriptor)) return true; if (model.customProperties.has(Descriptor)) return true;
let db: Database let db: Database
...@@ -258,7 +260,7 @@ export namespace AssemblySymmetry { ...@@ -258,7 +260,7 @@ export namespace AssemblySymmetry {
let result: AssemblySymmetryGraphQL.Query let result: AssemblySymmetryGraphQL.Query
const variables: AssemblySymmetryGraphQL.Variables = { pdbId: model.label.toLowerCase() }; const variables: AssemblySymmetryGraphQL.Variables = { pdbId: model.label.toLowerCase() };
try { try {
result = await client.request<AssemblySymmetryGraphQL.Query>(query, variables); result = await client.request<AssemblySymmetryGraphQL.Query>(ctx || RuntimeContext.Synchronous, query, variables);
} catch (e) { } catch (e) {
console.error(e) console.error(e)
return false; return false;
...@@ -273,6 +275,14 @@ export namespace AssemblySymmetry { ...@@ -273,6 +275,14 @@ export namespace AssemblySymmetry {
return true; return true;
} }
export function createAttachTask(fetch: (url: string, type: 'string' | 'binary') => Task<string | Uint8Array>) {
return (model: Model) => Task.create('RCSB Assembly Symmetry', async ctx => {
if (get(model)) return true;
return await attachFromCifOrAPI(model, new GraphQLClient(AssemblySymmetry.GraphQLEndpointURL, fetch), ctx)
});
}
export function get(model: Model): AssemblySymmetry | undefined { export function get(model: Model): AssemblySymmetry | undefined {
return model._staticPropertyData.__RCSBAssemblySymmetry__; return model._staticPropertyData.__RCSBAssemblySymmetry__;
} }
......
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { ThemeDataContext } from 'mol-theme/theme';
import { ColorTheme, LocationColor } from 'mol-theme/color';
import { ParamDefinition as PD } from 'mol-util/param-definition'
import { Table } from 'mol-data/db';
import { AssemblySymmetry } from '../assembly-symmetry';
import { ColorScale, Color } from 'mol-util/color';
import { Unit, StructureElement, StructureProperties } from 'mol-model/structure';
import { Location } from 'mol-model/location';
import { ColorListName, ColorListOptions } from 'mol-util/color/scale';
const DefaultColor = Color(0xCCCCCC)
function getAsymId(unit: Unit): StructureElement.Property<string> {
switch (unit.kind) {
case Unit.Kind.Atomic:
return StructureProperties.chain.label_asym_id
case Unit.Kind.Spheres:
case Unit.Kind.Gaussians:
return StructureProperties.coarse.asym_id
}
}
function clusterMemberKey (asym_id: string, oper_list_ids: string[]) {
return `${asym_id}-${oper_list_ids.join('x')}`
}
export const AssemblySymmetryClusterColorThemeParams = {
list: PD.Select<ColorListName>('Viridis', ColorListOptions),
symmetryId: PD.Select<number>(0, []),
}
export type AssemblySymmetryClusterColorThemeParams = typeof AssemblySymmetryClusterColorThemeParams
export function getAssemblySymmetryClusterColorThemeParams(ctx: ThemeDataContext) {
const params = PD.clone(AssemblySymmetryClusterColorThemeParams)
if (ctx.structure && ctx.structure.models[0].customProperties.has(AssemblySymmetry.Descriptor)) {
const assemblySymmetry = AssemblySymmetry.get(ctx.structure.models[0])!
const assemblyName = ctx.structure.assemblyName
const s = assemblySymmetry.db.rcsb_assembly_symmetry
if (s._rowCount) {
params.symmetryId.options = []
for (let i = 0, il = s._rowCount; i < il; ++i) {
if (s.assembly_id.value(i) === assemblyName) {
params.symmetryId.options.push([
s.id.value(i), `${s.symbol.value(i)} ${s.kind.value(i)}`
])
}
}
params.symmetryId.defaultValue = s.id.value(0)
}
}
return params
}
export function AssemblySymmetryClusterColorTheme(ctx: ThemeDataContext, props: PD.Values<AssemblySymmetryClusterColorThemeParams>): ColorTheme<AssemblySymmetryClusterColorThemeParams> {
let color: LocationColor = () => DefaultColor
const { symmetryId } = props
if (ctx.structure && ctx.structure.models[0].customProperties.has(AssemblySymmetry.Descriptor)) {
const assemblySymmetry = AssemblySymmetry.get(ctx.structure.models[0])!
const s = assemblySymmetry.db.rcsb_assembly_symmetry
const symmetry = Table.pickRow(s, i => s.id.value(i) === symmetryId)
if (symmetry) {
const clusters = assemblySymmetry.getClusters(symmetryId)
if (clusters._rowCount) {
const clusterByMember = new Map<string, number>()
for (let i = 0, il = clusters._rowCount; i < il; ++i) {
const clusterMembers = assemblySymmetry.getClusterMembers(clusters.id.value(i))
for (let j = 0, jl = clusterMembers._rowCount; j < jl; ++j) {
const asym_id = clusterMembers.asym_id.value(j)
const oper_list_ids = clusterMembers.pdbx_struct_oper_list_ids.value(j)
if (oper_list_ids.length === 0) oper_list_ids.push('1') // TODO hack assuming '1' is the id of the identity operator
clusterByMember.set(clusterMemberKey(asym_id, oper_list_ids), i)
}
}
const scale = ColorScale.create({ listOrName: props.list, domain: [ 0, clusters._rowCount - 1 ] })
color = (location: Location): Color => {
if (StructureElement.isLocation(location)) {
const asym_id = getAsymId(location.unit)
const ns = location.unit.conformation.operator.name.split('-')
const oper_list_ids = ns.length === 2 ? ns[1].split('x') : []
const cluster = clusterByMember.get(clusterMemberKey(asym_id(location), oper_list_ids))
return cluster !== undefined ? scale.color(cluster) : DefaultColor
}
return DefaultColor
}
}
}
}
return {
factory: AssemblySymmetryClusterColorTheme,
granularity: 'instance',
color,
props,
description: 'Assigns chain colors according to assembly symmetry cluster membership.',
}
}
export const AssemblySymmetryClusterColorThemeProvider: ColorTheme.Provider<AssemblySymmetryClusterColorThemeParams> = {
label: 'RCSB Assembly Symmetry Cluster',
factory: AssemblySymmetryClusterColorTheme,
getParams: getAssemblySymmetryClusterColorThemeParams,
defaultValues: PD.getDefaultValues(AssemblySymmetryClusterColorThemeParams)
}
\ No newline at end of file
...@@ -2,78 +2,8 @@ ...@@ -2,78 +2,8 @@
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
* *
* @author David Sehnal <david.sehnal@gmail.com> * @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/ */
import { OrderedSet } from 'mol-data/int'; export { PDBeStructureQualityReport } from './custom-props/pdbe/structure-quality-report'
import { StructureQualityReport } from 'mol-model-props/pdbe/structure-quality-report'; export { RCSBAssemblySymmetry } from './custom-props/rcsb/assembly-symmetry'
import { StructureQualityReportColorTheme } from 'mol-model-props/pdbe/themes/structure-quality-report'; \ No newline at end of file
import { Loci } from 'mol-model/loci';
import { StructureElement } from 'mol-model/structure';
import { CustomPropertyRegistry } from 'mol-plugin/util/custom-prop-registry';
import { ParamDefinition as PD } from 'mol-util/param-definition';
import { PluginBehavior } from '../behavior';
export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: boolean }>({
name: 'pdbe-structure-quality-report-prop',
display: { name: 'PDBe Structure Quality Report', group: 'Custom Props' },
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
private attach = StructureQualityReport.createAttachTask(
m => `https://www.ebi.ac.uk/pdbe/api/validation/residuewise_outlier_summary/entry/${m.label.toLowerCase()}`,
this.ctx.fetch
);
private provider: CustomPropertyRegistry.Provider = {
option: [StructureQualityReport.Descriptor.name, 'PDBe Structure Quality Report'],
descriptor: StructureQualityReport.Descriptor,
defaultSelected: this.params.autoAttach,
attachableTo: () => true,
attach: this.attach
}
register(): void {
this.ctx.customModelProperties.register(this.provider);
this.ctx.lociLabels.addProvider(labelPDBeValidation);
// TODO: support filtering of themes based on the input structure
// in this case, it would check structure.models[0].customProperties.has(StructureQualityReport.Descriptor)
this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add('pdbe-structure-quality-report', {
label: 'PDBe Structure Quality Report',
factory: StructureQualityReportColorTheme,
getParams: () => ({}),
defaultValues: {}
})
}
update(p: { autoAttach: boolean }) {
let updated = this.params.autoAttach !== p.autoAttach
this.params.autoAttach = p.autoAttach;
this.provider.defaultSelected = p.autoAttach;
return updated;
}
unregister() {
this.ctx.customModelProperties.unregister(StructureQualityReport.Descriptor.name);
this.ctx.lociLabels.removeProvider(labelPDBeValidation);
this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove('pdbe-structure-quality-report')
}
},
params: () => ({
autoAttach: PD.Boolean(false)
})
});
function labelPDBeValidation(loci: Loci): string | undefined {
switch (loci.kind) {
case 'element-loci':
const e = loci.elements[0];
const u = e.unit;
if (!u.model.customProperties.has(StructureQualityReport.Descriptor)) return void 0;
const se = StructureElement.create(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(', ')}`;
default: return void 0;
}
}
\ No newline at end of file
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { OrderedSet } from 'mol-data/int';
import { StructureQualityReport } from 'mol-model-props/pdbe/structure-quality-report';
import { StructureQualityReportColorTheme } from 'mol-model-props/pdbe/themes/structure-quality-report';
import { Loci } from 'mol-model/loci';
import { StructureElement } from 'mol-model/structure';
import { CustomPropertyRegistry } from 'mol-plugin/util/custom-prop-registry';
import { ParamDefinition as PD } from 'mol-util/param-definition';
import { PluginBehavior } from '../../../behavior';
export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: boolean }>({
name: 'pdbe-structure-quality-report-prop',
display: { name: 'PDBe Structure Quality Report', group: 'Custom Props' },
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
private attach = StructureQualityReport.createAttachTask(
m => `https://www.ebi.ac.uk/pdbe/api/validation/residuewise_outlier_summary/entry/${m.label.toLowerCase()}`,
this.ctx.fetch
);
private provider: CustomPropertyRegistry.Provider = {
option: [StructureQualityReport.Descriptor.name, 'PDBe Structure Quality Report'],
descriptor: StructureQualityReport.Descriptor,
defaultSelected: this.params.autoAttach,
attachableTo: () => true,
attach: this.attach
}
register(): void {
this.ctx.customModelProperties.register(this.provider);
this.ctx.lociLabels.addProvider(labelPDBeValidation);
// TODO: support filtering of themes based on the input structure
// in this case, it would check structure.models[0].customProperties.has(StructureQualityReport.Descriptor)
this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add('pdbe-structure-quality-report', {
label: 'PDBe Structure Quality Report',
factory: StructureQualityReportColorTheme,
getParams: () => ({}),
defaultValues: {}
})
}
update(p: { autoAttach: boolean }) {
let updated = this.params.autoAttach !== p.autoAttach
this.params.autoAttach = p.autoAttach;
this.provider.defaultSelected = p.autoAttach;
return updated;
}
unregister() {
this.ctx.customModelProperties.unregister(StructureQualityReport.Descriptor.name);
this.ctx.lociLabels.removeProvider(labelPDBeValidation);
this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove('pdbe-structure-quality-report')
}
},
params: () => ({
autoAttach: PD.Boolean(false)
})
});
function labelPDBeValidation(loci: Loci): string | undefined {
switch (loci.kind) {
case 'element-loci':
const e = loci.elements[0];
const u = e.unit;
if (!u.model.customProperties.has(StructureQualityReport.Descriptor)) return void 0;
const se = StructureElement.create(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(', ')}`;
default: return void 0;
}
}
\ No newline at end of file
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { PluginBehavior } from 'mol-plugin/behavior';
import { ParamDefinition as PD } from 'mol-util/param-definition'
import { AssemblySymmetry } from 'mol-model-props/rcsb/assembly-symmetry';
import { CustomPropertyRegistry } from 'mol-plugin/util/custom-prop-registry';
import { AssemblySymmetryClusterColorThemeProvider } from 'mol-model-props/rcsb/themes/assembly-symmetry';
export const RCSBAssemblySymmetry = PluginBehavior.create<{ autoAttach: boolean }>({
name: 'rcsb-assembly-symmetry-prop',
display: { name: 'RCSB Assembly Symmetry', group: 'Custom Props' },
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
private attach = AssemblySymmetry.createAttachTask(this.ctx.fetch);
private provider: CustomPropertyRegistry.Provider = {
option: [AssemblySymmetry.Descriptor.name, 'RCSB Assembly Symmetry'],
descriptor: AssemblySymmetry.Descriptor,
defaultSelected: this.params.autoAttach,
attachableTo: () => true,
attach: this.attach
}
register(): void {
this.ctx.customModelProperties.register(this.provider);
// TODO: support filtering of themes based on the input structure
// in this case, it would check structure.models[0].customProperties.has(AssemblySymmetry.Descriptor)
this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add('rcsb-assembly-symmetry-cluster', AssemblySymmetryClusterColorThemeProvider)
}
update(p: { autoAttach: boolean }) {
let updated = this.params.autoAttach !== p.autoAttach
this.params.autoAttach = p.autoAttach;
this.provider.defaultSelected = p.autoAttach;
return updated;
}
unregister() {
this.ctx.customModelProperties.unregister(AssemblySymmetry.Descriptor.name);
this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove('rcsb-assembly-symmetry-cluster')
}
},
params: () => ({
autoAttach: PD.Boolean(false)
})
});
\ No newline at end of file
...@@ -36,7 +36,8 @@ const DefaultSpec: PluginSpec = { ...@@ -36,7 +36,8 @@ const DefaultSpec: PluginSpec = {
PluginSpec.Behavior(PluginBehaviors.Representation.SelectLoci), PluginSpec.Behavior(PluginBehaviors.Representation.SelectLoci),
PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider), PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider),
PluginSpec.Behavior(PluginBehaviors.Camera.FocusLociOnSelect, { minRadius: 20, extraRadius: 4 }), PluginSpec.Behavior(PluginBehaviors.Camera.FocusLociOnSelect, { minRadius: 20, extraRadius: 4 }),
PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: true }) PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: true }),
PluginSpec.Behavior(PluginBehaviors.CustomProps.RCSBAssemblySymmetry, { autoAttach: true }),
] ]
} }
......
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*
* Adapted from https://github.com/prisma/graphql-request, Copyright (c) 2017 Graphcool, MIT
*/
import { Task, RuntimeContext } from 'mol-task';
type Variables = { [key: string]: any }
interface GraphQLError {
message: string
locations: { line: number, column: number }[]
path: string[]
}
interface GraphQLResponse {
data?: any
errors?: GraphQLError[]
extensions?: any
status: number
[key: string]: any
}
interface GraphQLRequestContext {
query: string
variables?: Variables
}
export class ClientError extends Error {
response: GraphQLResponse
request: GraphQLRequestContext
constructor (response: GraphQLResponse, request: GraphQLRequestContext) {
const message = `${ClientError.extractMessage(response)}: ${JSON.stringify({ response, request })}`
super(message)
this.response = response
this.request = request
// this is needed as Safari doesn't support .captureStackTrace
/* tslint:disable-next-line */
if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, ClientError)
}
}
private static extractMessage (response: GraphQLResponse): string {
try {
return response.errors![0].message
} catch (e) {
return `GraphQL Error (Code: ${response.status})`
}
}
}
export class GraphQLClient {
constructor(private url: string, private fetch: (url: string, type: 'string' | 'binary', body?: string) => Task<string | Uint8Array>) {
this.url = url
}
async request<T extends any>(ctx: RuntimeContext, query: string, variables?: Variables): Promise<T> {
const body = JSON.stringify({
query,
variables: variables ? variables : undefined,
})
const resultStr = await this.fetch(this.url, 'string', body).runInContext(ctx) as string
const result = JSON.parse(resultStr)
if (!result.errors && result.data) {
return result.data
} else {
const errorResult = typeof result === 'string' ? { error: result } : result
throw new ClientError(
{ ...errorResult },
{ query, variables },
)
}
}
}
\ No newline at end of file
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
* @author Alexander Rose <alexander.rose@weirdbyte.de> * @author Alexander Rose <alexander.rose@weirdbyte.de>
*/ */
import { AssemblySymmetry } from 'mol-model-props/rcsb/symmetry'; import { AssemblySymmetry } from 'mol-model-props/rcsb/assembly-symmetry';
import { AttachModelProperty } from '../../property-provider'; import { AttachModelProperty } from '../../property-provider';
export const RCSB_assemblySymmetry: AttachModelProperty = ({ model }) => { export const RCSB_assemblySymmetry: AttachModelProperty = ({ model }) => {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment