Skip to content
Snippets Groups Projects
structure-quality-report.ts 4.48 KiB
/**
 * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
 *
 * @author David Sehnal <david.sehnal@gmail.com>
 */

import { Segmentation } from 'mol-data/int';
import { CifWriter } from 'mol-io/writer/cif';
import { Model, ModelPropertyDescriptor, ResidueIndex, Structure, StructureElement, Unit } from 'mol-model/structure';
import { residueIdFields } from 'mol-model/structure/export/categories/atom_site';
import CifField = CifWriter.Field;

type IssueMap = Map<ResidueIndex, string[]>

const _Descriptor: ModelPropertyDescriptor = {
    isStatic: true,
    name: 'structure_quality_report',
    cifExport: {
        prefix: 'pdbe',
        categories: [{
            name: 'pdbe_structure_quality_report',
            instance(ctx) {
                const issues = StructureQualityReport.get(ctx.model);
                if (!issues) return CifWriter.Category.Empty;

                const residues = getResidueLoci(ctx.structure, issues);
                return {
                    fields: _structure_quality_report_fields,
                    data: <ExportCtx>{ model: ctx.model, residues, residueIndex: ctx.model.atomicHierarchy.residueAtomSegments.index, issues },
                    rowCount: residues.length
                };
            }
        }]
    }
}

type ExportCtx = { model: Model, residueIndex: ArrayLike<ResidueIndex>, residues: StructureElement[], issues: IssueMap };

const _structure_quality_report_fields: CifField<ResidueIndex, ExportCtx>[] = [
    CifField.index('id'),
    ...residueIdFields<ResidueIndex, ExportCtx>((k, d) => d.residues[k]),
    CifField.str<ResidueIndex, ExportCtx>('issues', (i, d) => d.issues.get(d.residueIndex[d.residues[i].element])!.join(','))
];

function getResidueLoci(structure: Structure, issues: IssueMap) {
    const seenResidues = new Set<ResidueIndex>();
    const unitGroups = structure.unitSymmetryGroups;
    const loci: StructureElement[] = [];

    for (const unitGroup of unitGroups) {
        const unit = unitGroup.units[0];
        if (!Unit.isAtomic(unit)) {
            continue;
        }

        const residues = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements);
        while (residues.hasNext) {
            const seg = residues.move();
            if (!issues.has(seg.index) || seenResidues.has(seg.index)) continue;

            seenResidues.add(seg.index);
            loci[loci.length] = StructureElement.create(unit, unit.elements[seg.start]);
        }
    }

    loci.sort((x, y) => x.element - y.element);
    return loci;
}

function createIssueMap(modelData: Model, data: any): IssueMap | undefined {
    const ret = new Map<ResidueIndex, string[]>();
    if (!data.molecules) return;

    for (const entity of data.molecules) {
        const entity_id = entity.entity_id.toString();
        for (const chain of entity.chains) {
            const asym_id = chain.struct_asym_id.toString();
            for (const model of chain.models) {
                const model_id = model.model_id.toString();
                if (+model_id !== modelData.modelNum) continue;

                for (const residue of model.residues) {
                    const auth_seq_id = residue.author_residue_number, ins_code = residue.author_insertion_code || '';
                    const idx = modelData.atomicHierarchy.findResidueKey(entity_id, asym_id, '', auth_seq_id, ins_code);
                    ret.set(idx, residue.outlier_types);
                }
            }
        }
    }

    return ret;
}

export namespace StructureQualityReport {
    export const Descriptor = _Descriptor;

    export async function attachFromCifOrApi(model: Model, params: {
        // provide JSON from api
        pdbEapiSourceJson?: (model: Model) => Promise<any>
    }) {
        // TODO: check if present in mmCIF on Model
        if (!params.pdbEapiSourceJson) throw new Error('Data source not defined');

        if (model.customProperties.has(Descriptor)) return true;

        const id = model.label.toLowerCase();
        const json = await params.pdbEapiSourceJson(model);
        const data = json[id];
        if (!data) return false;
        const issueMap = createIssueMap(model, data);
        if (!issueMap || issueMap.size === 0) return false;

        model.customProperties.add(Descriptor);
        model._staticPropertyData.__StructureQualityReport__ = issueMap;
        return true;
    }

    export function get(model: Model): IssueMap | undefined {
        return model._staticPropertyData.__StructureQualityReport__;
    }
}