symmetry-operator.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. /**
  2. * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  6. */
  7. import { lerp as scalar_lerp } from '../../mol-math/interpolate';
  8. import { Mat3 } from '../linear-algebra/3d/mat3';
  9. import { Mat4 } from '../linear-algebra/3d/mat4';
  10. import { Quat } from '../linear-algebra/3d/quat';
  11. import { Vec3 } from '../linear-algebra/3d/vec3';
  12. interface SymmetryOperator {
  13. readonly name: string,
  14. readonly assembly?: {
  15. /** pointer to `pdbx_struct_assembly.id` or empty string */
  16. readonly id: string,
  17. /** pointers to `pdbx_struct_oper_list.id` or empty list */
  18. readonly operList: string[],
  19. /** (arbitrary) unique id of the operator to be used in suffix */
  20. readonly operId: number
  21. },
  22. /** pointer to `struct_ncs_oper.id` or -1 */
  23. readonly ncsId: number,
  24. readonly hkl: Vec3,
  25. /** spacegroup symmetry operator index, -1 if not applicable */
  26. readonly spgrOp: number,
  27. /** unique (external) key, -1 if not available */
  28. readonly key: number,
  29. readonly matrix: Mat4,
  30. /** cache the inverse of the transform */
  31. readonly inverse: Mat4,
  32. /** optimize the identity case */
  33. readonly isIdentity: boolean,
  34. /**
  35. * Suffix based on operator type.
  36. * - Assembly: _assembly.operId
  37. * - Crytal: -op_ijk
  38. * - ncs: _ncsId
  39. */
  40. readonly suffix: string
  41. }
  42. namespace SymmetryOperator {
  43. export const DefaultName = '1_555';
  44. export const Default: SymmetryOperator = create(DefaultName, Mat4.identity());
  45. export const RotationTranslationEpsilon = 0.005;
  46. export type CreateInfo = { assembly?: SymmetryOperator['assembly'], ncsId?: number, hkl?: Vec3, spgrOp?: number, key?: number }
  47. export function create(name: string, matrix: Mat4, info?: CreateInfo | SymmetryOperator): SymmetryOperator {
  48. let { assembly, ncsId, hkl, spgrOp, key } = info || { };
  49. const _hkl = hkl ? Vec3.clone(hkl) : Vec3();
  50. spgrOp = spgrOp ?? -1;
  51. key = key ?? -1;
  52. ncsId = ncsId || -1;
  53. const isIdentity = Mat4.isIdentity(matrix);
  54. const suffix = getSuffix(info, isIdentity);
  55. if (isIdentity) return { name, assembly, matrix, inverse: Mat4.identity(), isIdentity: true, hkl: _hkl, spgrOp, ncsId, suffix, key };
  56. if (!Mat4.isRotationAndTranslation(matrix, RotationTranslationEpsilon)) {
  57. console.warn(`Symmetry operator (${name}) should be a composition of rotation and translation.`);
  58. }
  59. return { name, assembly, matrix, inverse: Mat4.invert(Mat4(), matrix), isIdentity: false, hkl: _hkl, spgrOp, key, ncsId, suffix };
  60. }
  61. function isSymmetryOperator(x: any): x is SymmetryOperator {
  62. return !!x && !!x.matrix && !!x.inverse && typeof x.name === 'string';
  63. }
  64. function getSuffix(info?: CreateInfo | SymmetryOperator, isIdentity?: boolean) {
  65. if (!info) return '';
  66. if (info.assembly) {
  67. if (isSymmetryOperator(info)) return info.suffix;
  68. return isIdentity ? '' : `_${info.assembly.operId}`;
  69. }
  70. if (typeof info.spgrOp !== 'undefined' && typeof info.hkl !== 'undefined' && info.spgrOp !== -1) {
  71. const [i, j, k] = info.hkl;
  72. return `-${info.spgrOp + 1}_${5 + i}${5 + j}${5 + k}`;
  73. }
  74. if (info.ncsId !== -1) {
  75. return `_${info.ncsId}`;
  76. }
  77. return '';
  78. }
  79. export function checkIfRotationAndTranslation(rot: Mat3, offset: Vec3) {
  80. const matrix = Mat4.identity();
  81. for (let i = 0; i < 3; i++) {
  82. for (let j = 0; j < 3; j++) {
  83. Mat4.setValue(matrix, i, j, Mat3.getValue(rot, i, j));
  84. }
  85. }
  86. Mat4.setTranslation(matrix, offset);
  87. return Mat4.isRotationAndTranslation(matrix, RotationTranslationEpsilon);
  88. }
  89. export function ofRotationAndOffset(name: string, rot: Mat3, offset: Vec3, ncsId?: number) {
  90. const t = Mat4.identity();
  91. for (let i = 0; i < 3; i++) {
  92. for (let j = 0; j < 3; j++) {
  93. Mat4.setValue(t, i, j, Mat3.getValue(rot, i, j));
  94. }
  95. }
  96. Mat4.setTranslation(t, offset);
  97. return create(name, t, { ncsId });
  98. }
  99. const _q1 = Quat.identity(), _q2 = Quat(), _q3 = Quat(), _axis = Vec3();
  100. export function lerpFromIdentity(out: Mat4, op: SymmetryOperator, t: number): Mat4 {
  101. const m = op.inverse;
  102. if (op.isIdentity) return Mat4.copy(out, m);
  103. const _t = 1 - t;
  104. // interpolate rotation
  105. Mat4.getRotation(_q2, m);
  106. Quat.slerp(_q2, _q1, _q2, _t);
  107. const angle = Quat.getAxisAngle(_axis, _q2);
  108. Mat4.fromRotation(out, angle, _axis);
  109. // interpolate translation
  110. Mat4.setValue(out, 0, 3, _t * Mat4.getValue(m, 0, 3));
  111. Mat4.setValue(out, 1, 3, _t * Mat4.getValue(m, 1, 3));
  112. Mat4.setValue(out, 2, 3, _t * Mat4.getValue(m, 2, 3));
  113. return out;
  114. }
  115. export function slerp(out: Mat4, src: Mat4, tar: Mat4, t: number): Mat4 {
  116. if (Math.abs(t) <= 0.00001) return Mat4.copy(out, src);
  117. if (Math.abs(t - 1) <= 0.00001) return Mat4.copy(out, tar);
  118. // interpolate rotation
  119. Mat4.getRotation(_q2, src);
  120. Mat4.getRotation(_q3, tar);
  121. Quat.slerp(_q3, _q2, _q3, t);
  122. const angle = Quat.getAxisAngle(_axis, _q3);
  123. Mat4.fromRotation(out, angle, _axis);
  124. // interpolate translation
  125. Mat4.setValue(out, 0, 3, scalar_lerp(Mat4.getValue(src, 0, 3), Mat4.getValue(tar, 0, 3), t));
  126. Mat4.setValue(out, 1, 3, scalar_lerp(Mat4.getValue(src, 1, 3), Mat4.getValue(tar, 1, 3), t));
  127. Mat4.setValue(out, 2, 3, scalar_lerp(Mat4.getValue(src, 2, 3), Mat4.getValue(tar, 2, 3), t));
  128. return out;
  129. }
  130. /**
  131. * Apply the 1st and then 2nd operator. ( = second.matrix * first.matrix).
  132. * Keep `name`, `assembly`, `ncsId`, `hkl` and `spgrOpId` properties from second.
  133. */
  134. export function compose(first: SymmetryOperator, second: SymmetryOperator) {
  135. const matrix = Mat4.mul(Mat4(), second.matrix, first.matrix);
  136. return create(second.name, matrix, second);
  137. }
  138. export interface CoordinateMapper<T extends number> { (index: T, slot: Vec3): Vec3 }
  139. export interface ArrayMapping<T extends number> {
  140. readonly coordinates: Coordinates,
  141. readonly operator: SymmetryOperator,
  142. readonly invariantPosition: CoordinateMapper<T>,
  143. readonly position: CoordinateMapper<T>,
  144. x(index: T): number,
  145. y(index: T): number,
  146. z(index: T): number,
  147. r(index: T): number
  148. }
  149. export interface Coordinates { x: ArrayLike<number>, y: ArrayLike<number>, z: ArrayLike<number> }
  150. export function createMapping<T extends number>(operator: SymmetryOperator, coords: Coordinates, radius: ((index: T) => number)): ArrayMapping<T> {
  151. const invariantPosition = createCoordinateMapper(SymmetryOperator.Default, coords);
  152. const position = operator.isIdentity ? invariantPosition : createCoordinateMapper(operator, coords);
  153. const { x, y, z } = createProjections(operator, coords);
  154. return { operator, coordinates: coords, invariantPosition, position, x, y, z, r: radius };
  155. }
  156. export function createMappingZeroRadius<T extends number>(operator: SymmetryOperator, coords: Coordinates): ArrayMapping<T> {
  157. return createMapping(operator, coords, _zeroRadius);
  158. }
  159. export function createCoordinateMapper<T extends number>(t: SymmetryOperator, coords: Coordinates): CoordinateMapper<T> {
  160. if (t.isIdentity) return identityPosition(coords);
  161. return generalPosition(t, coords);
  162. }
  163. }
  164. export { SymmetryOperator };
  165. function _zeroRadius(i: number) { return 0; }
  166. interface Projections { x(index: number): number, y(index: number): number, z(index: number): number }
  167. function createProjections(t: SymmetryOperator, coords: SymmetryOperator.Coordinates): Projections {
  168. if (t.isIdentity) return { x: projectCoord(coords.x), y: projectCoord(coords.y), z: projectCoord(coords.z) };
  169. return { x: projectX(t, coords), y: projectY(t, coords), z: projectZ(t, coords) };
  170. }
  171. function projectCoord(xs: ArrayLike<number>) {
  172. return function projectCoord(i: number) {
  173. return xs[i];
  174. };
  175. }
  176. function isW1(m: Mat4) {
  177. return m[3] === 0 && m[7] === 0 && m[11] === 0 && m[15] === 1;
  178. }
  179. function projectX({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs }: SymmetryOperator.Coordinates) {
  180. const xx = m[0], yy = m[4], zz = m[8], tx = m[12];
  181. if (isW1(m)) {
  182. // this should always be the case.
  183. return function projectX_W1(i: number) {
  184. return xx * xs[i] + yy * ys[i] + zz * zs[i] + tx;
  185. };
  186. }
  187. return function projectX(i: number) {
  188. const x = xs[i], y = ys[i], z = zs[i], w = (m[3] * x + m[7] * y + m[11] * z + m[15]) || 1.0;
  189. return (xx * x + yy * y + zz * z + tx) / w;
  190. };
  191. }
  192. function projectY({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs }: SymmetryOperator.Coordinates) {
  193. const xx = m[1], yy = m[5], zz = m[9], ty = m[13];
  194. if (isW1(m)) {
  195. // this should always be the case.
  196. return function projectY_W1(i: number) {
  197. return xx * xs[i] + yy * ys[i] + zz * zs[i] + ty;
  198. };
  199. }
  200. return function projectY(i: number) {
  201. const x = xs[i], y = ys[i], z = zs[i], w = (m[3] * x + m[7] * y + m[11] * z + m[15]) || 1.0;
  202. return (xx * x + yy * y + zz * z + ty) / w;
  203. };
  204. }
  205. function projectZ({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs }: SymmetryOperator.Coordinates) {
  206. const xx = m[2], yy = m[6], zz = m[10], tz = m[14];
  207. if (isW1(m)) {
  208. // this should always be the case.
  209. return function projectZ_W1(i: number) {
  210. return xx * xs[i] + yy * ys[i] + zz * zs[i] + tz;
  211. };
  212. }
  213. return function projectZ(i: number) {
  214. const x = xs[i], y = ys[i], z = zs[i], w = (m[3] * x + m[7] * y + m[11] * z + m[15]) || 1.0;
  215. return (xx * x + yy * y + zz * z + tz) / w;
  216. };
  217. }
  218. function identityPosition<T extends number>({ x, y, z }: SymmetryOperator.Coordinates): SymmetryOperator.CoordinateMapper<T> {
  219. return function identityPosition(i: T, s: Vec3): Vec3 {
  220. s[0] = x[i];
  221. s[1] = y[i];
  222. s[2] = z[i];
  223. return s;
  224. };
  225. }
  226. function generalPosition<T extends number>({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs }: SymmetryOperator.Coordinates) {
  227. if (isW1(m)) {
  228. // this should always be the case.
  229. return function generalPosition_W1(i: T, r: Vec3): Vec3 {
  230. const x = xs[i], y = ys[i], z = zs[i];
  231. r[0] = m[0] * x + m[4] * y + m[8] * z + m[12];
  232. r[1] = m[1] * x + m[5] * y + m[9] * z + m[13];
  233. r[2] = m[2] * x + m[6] * y + m[10] * z + m[14];
  234. return r;
  235. };
  236. }
  237. return function generalPosition(i: T, r: Vec3): Vec3 {
  238. r[0] = xs[i];
  239. r[1] = ys[i];
  240. r[2] = zs[i];
  241. Vec3.transformMat4(r, r, m);
  242. return r;
  243. };
  244. }