From f94007bef2250f613d8a96eacaa1e517d165bdd3 Mon Sep 17 00:00:00 2001
From: David Sehnal <david.sehnal@gmail.com>
Date: Fri, 15 Jun 2018 11:43:08 +0200
Subject: [PATCH] Parse and generate NCS operators

---
 data/mmcif-field-names.csv                    |  6 +++
 src/apps/structure-info/model.ts              | 12 ++++++
 src/mol-io/reader/cif/schema/mmcif.ts         |  7 ++++
 src/mol-math/geometry/symmetry-operator.ts    | 13 ++++++-
 src/mol-math/linear-algebra/3d/mat3.ts        |  4 ++
 src/mol-math/linear-algebra/3d/vec3.ts        |  4 ++
 .../structure/model/formats/mmcif.ts          | 24 ++++++++++--
 .../structure/model/properties/symmetry.ts    |  1 +
 src/mol-model/structure/structure/symmetry.ts | 37 +++++++++++++------
 9 files changed, 92 insertions(+), 16 deletions(-)

diff --git a/data/mmcif-field-names.csv b/data/mmcif-field-names.csv
index bd1f2d51b..45b870d17 100644
--- a/data/mmcif-field-names.csv
+++ b/data/mmcif-field-names.csv
@@ -144,6 +144,12 @@ struct_keywords.entry_id
 struct_keywords.pdbx_keywords
 struct_keywords.text
 
+struct_ncs_oper.id
+struct_ncs_oper.code
+struct_ncs_oper.matrix
+struct_ncs_oper.vector
+struct_ncs_oper.details
+
 struct_sheet_range.sheet_id
 struct_sheet_range.id
 struct_sheet_range.beg_label_comp_id
diff --git a/src/apps/structure-info/model.ts b/src/apps/structure-info/model.ts
index 304f34fc1..d63913dca 100644
--- a/src/apps/structure-info/model.ts
+++ b/src/apps/structure-info/model.ts
@@ -18,6 +18,7 @@ import { openCif, downloadCif } from './helpers';
 import { BitFlags } from 'mol-util';
 import { SecondaryStructureType } from 'mol-model/structure/model/types';
 import { UnitRings } from 'mol-model/structure/structure/unit/rings';
+import { Vec3 } from 'mol-math/linear-algebra';
 
 
 async function downloadFromPdb(pdb: string) {
@@ -182,6 +183,16 @@ export function printUnits(structure: Structure) {
     }
 }
 
+export function printSymmetryInfo(model: Model) {
+    console.log('\nSymmetry Info\n=============');
+    const { symmetry } = model;
+    const { size, anglesInRadians } = symmetry.spacegroup.cell;
+    console.log(`Spacegroup: ${symmetry.spacegroup.name} size: ${Vec3.toString(size)} angles: ${Vec3.toString(anglesInRadians)}`);
+    console.log(`Assembly names: ${symmetry.assemblies.map(a => a.id).join(', ')}`);
+    // NCS example: 1auy
+    console.log(`NCS operators: ${symmetry.ncsOperators && symmetry.ncsOperators.map(a => a.name).join(', ')}`);
+}
+
 export function printIHMModels(model: Model) {
     if (!model.coarseHierarchy.isDefined) return false;
     console.log('\nIHM Models\n=============');
@@ -194,6 +205,7 @@ async function run(frame: CifFrame) {
     //printSequence(models[0]);
     //printIHMModels(models[0]);
     printUnits(structure);
+    printSymmetryInfo(models[0]);
     //printRings(structure);
     //printLinks(structure, true, true);
     //printModRes(models[0]);
diff --git a/src/mol-io/reader/cif/schema/mmcif.ts b/src/mol-io/reader/cif/schema/mmcif.ts
index 283b713dc..19ac89f35 100644
--- a/src/mol-io/reader/cif/schema/mmcif.ts
+++ b/src/mol-io/reader/cif/schema/mmcif.ts
@@ -409,6 +409,13 @@ export const mmCIF_Schema = {
         experiment_type: Aliased<'Fraction of bulk' | 'Single molecule'>(str),
         details: str,
     },
+    struct_ncs_oper: {
+        id: str,
+        code: str,
+        matrix: Matrix(3, 3),
+        vector: Vector(3),
+        details: str
+    },
     ihm_modeling_post_process: {
         id: int,
         protocol_id: int,
diff --git a/src/mol-math/geometry/symmetry-operator.ts b/src/mol-math/geometry/symmetry-operator.ts
index 2e9776d8d..890472d04 100644
--- a/src/mol-math/geometry/symmetry-operator.ts
+++ b/src/mol-math/geometry/symmetry-operator.ts
@@ -4,7 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { Vec3, Mat4 } from '../linear-algebra/3d'
+import { Vec3, Mat4, Mat3 } from '../linear-algebra/3d'
 
 interface SymmetryOperator {
     readonly name: string,
@@ -30,6 +30,17 @@ namespace SymmetryOperator {
         return { name, matrix, inverse: Mat4.invert(Mat4.zero(), matrix), isIdentity: false, hkl: _hkl };
     }
 
+    export function ofRotationAndOffset(name: string, rot: Mat3, offset: Vec3) {
+        const t = Mat4.identity();
+        for (let i = 0; i < 3; i++) {
+            for (let j = 0; j < 3; j++) {
+                Mat4.setValue(t, i, j, Mat3.getValue(rot, i, j));
+            }
+        }
+        Mat4.setTranslation(t, offset);
+        return create(name, t);
+    }
+
     // Apply the 1st and then 2nd operator. ( = second.matrix * first.matrix)
     export function compose(first: SymmetryOperator, second: SymmetryOperator) {
         const matrix = Mat4.mul(Mat4.zero(), second.matrix, first.matrix);
diff --git a/src/mol-math/linear-algebra/3d/mat3.ts b/src/mol-math/linear-algebra/3d/mat3.ts
index c72dc713d..c8a24df0e 100644
--- a/src/mol-math/linear-algebra/3d/mat3.ts
+++ b/src/mol-math/linear-algebra/3d/mat3.ts
@@ -108,6 +108,10 @@ namespace Mat3 {
         a[3 * j + i] = value;
     }
 
+    export function getValue(a: Mat3, i: number, j: number) {
+        return a[3 * j + i];
+    }
+
     /**
      * Copy the values from one Mat3 to another
      */
diff --git a/src/mol-math/linear-algebra/3d/vec3.ts b/src/mol-math/linear-algebra/3d/vec3.ts
index fc7c22999..2cbbea1e1 100644
--- a/src/mol-math/linear-algebra/3d/vec3.ts
+++ b/src/mol-math/linear-algebra/3d/vec3.ts
@@ -367,6 +367,10 @@ namespace Vec3 {
     export function isZero(v: Vec3) {
         return v[0] === 0 && v[1] === 0 && v[2] === 0
     }
+
+    export function toString(a: Vec3) {
+        return `[${a[0]} ${a[1]} ${a[2]}]`;
+    }
 }
 
 export default Vec3
\ No newline at end of file
diff --git a/src/mol-model/structure/model/formats/mmcif.ts b/src/mol-model/structure/model/formats/mmcif.ts
index 586b86a60..36ac1b011 100644
--- a/src/mol-model/structure/model/formats/mmcif.ts
+++ b/src/mol-model/structure/model/formats/mmcif.ts
@@ -6,8 +6,8 @@
 
 import { Column, Table } from 'mol-data/db';
 import { Interval, Segmentation } from 'mol-data/int';
-import { Spacegroup, SpacegroupCell } from 'mol-math/geometry';
-import { Vec3 } from 'mol-math/linear-algebra';
+import { Spacegroup, SpacegroupCell, SymmetryOperator } from 'mol-math/geometry';
+import { Vec3, Tensor, Mat4 } from 'mol-math/linear-algebra';
 import { Task } from 'mol-task';
 import UUID from 'mol-util/uuid';
 import Format from '../format';
@@ -22,7 +22,7 @@ import { getIHMCoarse } from './mmcif/ihm';
 import { getSecondaryStructureMmCif } from './mmcif/secondary-structure';
 import { getSequence } from './mmcif/sequence';
 import { sortAtomSite } from './mmcif/sort';
-import { mmCIF_Database } from 'mol-io/reader/cif/schema/mmcif';
+import { mmCIF_Database, mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif';
 
 import mmCIF_Format = Format.mmCIF
 type AtomSite = mmCIF_Database['atom_site']
@@ -90,7 +90,7 @@ function getSymmetry(format: mmCIF_Format): ModelSymmetry {
     const assemblies = createAssemblies(format);
     const spacegroup = getSpacegroup(format);
     const isNonStandardCrytalFrame = checkNonStandardCrystalFrame(format, spacegroup);
-    return { assemblies, spacegroup, isNonStandardCrytalFrame };
+    return { assemblies, spacegroup, isNonStandardCrytalFrame, ncsOperators: getNcsOperators(format) };
 }
 
 function checkNonStandardCrystalFrame(format: mmCIF_Format, spacegroup: Spacegroup) {
@@ -111,6 +111,22 @@ function getSpacegroup(format: mmCIF_Format): Spacegroup {
     return Spacegroup.create(spaceCell);
 }
 
+function getNcsOperators(format: mmCIF_Format) {
+    const { struct_ncs_oper } = format.data;
+    if (struct_ncs_oper._rowCount === 0) return void 0;
+    const { id, matrix, vector } = struct_ncs_oper;
+
+    const matrixSpace = mmCIF_Schema.struct_ncs_oper.matrix.space, vectorSpace = mmCIF_Schema.struct_ncs_oper.vector.space;
+
+    const opers: SymmetryOperator[] = [];
+    for (let i = 0; i < struct_ncs_oper._rowCount; i++) {
+        const m = Tensor.toMat3(matrixSpace, matrix.value(i));
+        const v = Tensor.toVec3(vectorSpace, vector.value(i));
+        opers[i] = SymmetryOperator.ofRotationAndOffset(`ncs_${id.value(i)}`, m, v);
+    }
+    return opers;
+}
+
 function isHierarchyDataEqual(a: AtomicData, b: AtomicData) {
     // need to cast because of how TS handles type resolution for interfaces https://github.com/Microsoft/TypeScript/issues/15300
     return Table.areEqual(a.chains as Table<ChainsSchema>, b.chains as Table<ChainsSchema>)
diff --git a/src/mol-model/structure/model/properties/symmetry.ts b/src/mol-model/structure/model/properties/symmetry.ts
index 883c35944..fab3c3798 100644
--- a/src/mol-model/structure/model/properties/symmetry.ts
+++ b/src/mol-model/structure/model/properties/symmetry.ts
@@ -45,6 +45,7 @@ interface ModelSymmetry {
     readonly assemblies: ReadonlyArray<Assembly>,
     readonly spacegroup: Spacegroup,
     readonly isNonStandardCrytalFrame: boolean,
+    readonly ncsOperators?: ReadonlyArray<SymmetryOperator>,
 
     // optionally cached operators from [-3, -3, -3] to [3, 3, 3]
     _operators_333?: SymmetryOperator[]
diff --git a/src/mol-model/structure/structure/symmetry.ts b/src/mol-model/structure/structure/symmetry.ts
index bd836b5d4..4b733f2ed 100644
--- a/src/mol-model/structure/structure/symmetry.ts
+++ b/src/mol-model/structure/structure/symmetry.ts
@@ -51,6 +51,11 @@ namespace StructureSymmetry {
         return Task.create('Build Symmetry', ctx => findSymmetryRange(ctx, structure, ijkMin, ijkMax));
     }
 
+    /** Builds NCS structure, returns the original if NCS operators are not present. */
+    export function buildNcs(structure: Structure) {
+        return Task.create('Build NCS', ctx => _buildNCS(ctx, structure));
+    }
+
     function hashUnit(u: Unit) {
         return hash2(u.invariantId, SortedArray.hashCode(u.elements));
     }
@@ -98,27 +103,37 @@ function getOperators(symmetry: ModelSymmetry, ijkMin: Vec3, ijkMax: Vec3) {
     return operators;
 }
 
-async function findSymmetryRange(ctx: RuntimeContext, structure: Structure, ijkMin: Vec3, ijkMax: Vec3) {
-    const models = Structure.getModels(structure);
-    if (models.length !== 1) throw new Error('Can only build symmetries from structures based on 1 model.');
-
-    const { spacegroup } = models[0].symmetry;
-    if (SpacegroupCell.isZero(spacegroup.cell)) return structure;
-
-    const operators = getOperators(models[0].symmetry, ijkMin, ijkMax);
+function assembleOperators(structure: Structure, operators: ReadonlyArray<SymmetryOperator>) {
     const assembler = Structure.Builder();
-
     const { units } = structure;
     for (const oper of operators) {
         for (const unit of units) {
             assembler.addWithOperator(unit, oper);
         }
-        if (ctx.shouldUpdate) await ctx.update('Building symmetry...');
     }
-
     return assembler.getStructure();
 }
 
+async function _buildNCS(ctx: RuntimeContext, structure: Structure) {
+    const models = Structure.getModels(structure);
+    if (models.length !== 1) throw new Error('Can only build NCS from structures based on 1 model.');
+
+    const operators = models[0].symmetry.ncsOperators;
+    if (!operators || !operators.length) return structure;
+    return assembleOperators(structure, operators);
+}
+
+async function findSymmetryRange(ctx: RuntimeContext, structure: Structure, ijkMin: Vec3, ijkMax: Vec3) {
+    const models = Structure.getModels(structure);
+    if (models.length !== 1) throw new Error('Can only build symmetries from structures based on 1 model.');
+
+    const { spacegroup } = models[0].symmetry;
+    if (SpacegroupCell.isZero(spacegroup.cell)) return structure;
+
+    const operators = getOperators(models[0].symmetry, ijkMin, ijkMax);
+    return assembleOperators(structure, operators);
+}
+
 async function findMatesRadius(ctx: RuntimeContext, structure: Structure, radius: number) {
     const models = Structure.getModels(structure);
     if (models.length !== 1) throw new Error('Can only build symmetries from structures based on 1 model.');
-- 
GitLab