validation-report.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. /**
  2. * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. */
  6. import { ParamDefinition as PD } from '../../mol-util/param-definition'
  7. import { CustomPropertyDescriptor, Structure, Unit } from '../../mol-model/structure';
  8. import { CustomProperty } from '../common/custom-property';
  9. import { CustomModelProperty } from '../common/custom-model-property';
  10. import { Model, ElementIndex, ResidueIndex } from '../../mol-model/structure/model';
  11. import { IntAdjacencyGraph } from '../../mol-math/graph';
  12. import { readFromFile } from '../../mol-util/data-source';
  13. import { CustomStructureProperty } from '../common/custom-structure-property';
  14. import { InterUnitGraph } from '../../mol-math/graph/inter-unit-graph';
  15. import { UnitIndex } from '../../mol-model/structure/structure/element/element';
  16. import { IntMap, SortedArray } from '../../mol-data/int';
  17. import { arrayMax } from '../../mol-util/array';
  18. import { equalEps } from '../../mol-math/linear-algebra/3d/common';
  19. import { Vec3 } from '../../mol-math/linear-algebra';
  20. import { MmcifFormat } from '../../mol-model-formats/structure/mmcif';
  21. import { QuerySymbolRuntime } from '../../mol-script/runtime/query/compiler';
  22. import { CustomPropSymbol } from '../../mol-script/language/symbol';
  23. import Type from '../../mol-script/language/type';
  24. export { ValidationReport }
  25. interface ValidationReport {
  26. /**
  27. * Real Space R (RSRZ) for residues,
  28. * defined for polymer residues in X-ray structures
  29. */
  30. rsrz: Map<ResidueIndex, number>
  31. /**
  32. * Real Space Correlation Coefficient (RSCC) for residues,
  33. * defined for each non-polymer residue in X-ray structures
  34. */
  35. rscc: Map<ResidueIndex, number>
  36. /**
  37. * Random Coil Index (RCI) for residues,
  38. * defined for polymer residues in NMR structures
  39. */
  40. rci: Map<ResidueIndex, number>
  41. /**
  42. * Set of geometry issues for residues
  43. */
  44. geometryIssues: Map<ResidueIndex, Set<string>>
  45. /**
  46. * Set of bond outliers
  47. */
  48. bondOutliers: {
  49. index: Map<ElementIndex, number[]>
  50. data: {
  51. tag: string, atomA: ElementIndex, atomB: ElementIndex
  52. z: number, mean: number, obs: number, stdev: number
  53. }[]
  54. }
  55. /**
  56. * Set of angle outliers
  57. */
  58. angleOutliers: {
  59. index: Map<ElementIndex, number[]>
  60. data: {
  61. tag: string, atomA: ElementIndex, atomB: ElementIndex, atomC: ElementIndex,
  62. z: number, mean: number, obs: number, stdev: number
  63. }[]
  64. }
  65. /**
  66. * Clashes between atoms, including id, magniture and distance
  67. */
  68. clashes: IntAdjacencyGraph<ElementIndex, {
  69. readonly id: ArrayLike<number>
  70. readonly magnitude: ArrayLike<number>
  71. readonly distance: ArrayLike<number>
  72. }>
  73. }
  74. namespace ValidationReport {
  75. export enum Tag {
  76. DensityFit = 'rcsb-density-fit',
  77. GeometryQuality = 'rcsb-geometry-quality',
  78. RandomCoilIndex = 'rcsb-random-coil-index',
  79. Clashes = 'rcsb-clashes',
  80. }
  81. export const DefaultBaseUrl = '//ftp.rcsb.org/pub/pdb/validation_reports'
  82. export function getEntryUrl(pdbId: string, baseUrl: string) {
  83. const id = pdbId.toLowerCase()
  84. return `${baseUrl}/${id.substr(1, 2)}/${id}/${id}_validation.xml.gz`
  85. }
  86. export function isApplicable(model?: Model): boolean {
  87. return (
  88. !!model &&
  89. MmcifFormat.is(model.sourceData) &&
  90. (model.sourceData.data.db.database_2.database_id.isDefined ||
  91. model.entryId.length === 4)
  92. )
  93. }
  94. export function fromXml(xml: XMLDocument, model: Model): ValidationReport {
  95. return parseValidationReportXml(xml, model)
  96. }
  97. export async function fetch(ctx: CustomProperty.Context, model: Model, props: ServerSourceProps): Promise<ValidationReport> {
  98. const url = getEntryUrl(model.entryId, props.baseUrl)
  99. const xml = await ctx.fetch({ url, type: 'xml' }).runInContext(ctx.runtime)
  100. return fromXml(xml, model)
  101. }
  102. export async function open(ctx: CustomProperty.Context, model: Model, props: FileSourceProps): Promise<ValidationReport> {
  103. if (props.input === null) throw new Error('No file given')
  104. const xml = await readFromFile(props.input, 'xml').runInContext(ctx.runtime)
  105. return fromXml(xml, model)
  106. }
  107. export async function obtain(ctx: CustomProperty.Context, model: Model, props: ValidationReportProps): Promise<ValidationReport> {
  108. switch(props.source.name) {
  109. case 'file': return open(ctx, model, props.source.params)
  110. case 'server': return fetch(ctx, model, props.source.params)
  111. }
  112. }
  113. export const symbols = {
  114. hasClash: QuerySymbolRuntime.Dynamic(CustomPropSymbol('rcsb', 'validation-report.has-clash', Type.Bool),
  115. ctx => {
  116. const { unit, element } = ctx.element
  117. if (!Unit.isAtomic(unit)) return 0
  118. const validationReport = ValidationReportProvider.get(unit.model).value
  119. return validationReport && validationReport.clashes.getVertexEdgeCount(element) > 0
  120. }
  121. ),
  122. issueCount: QuerySymbolRuntime.Dynamic(CustomPropSymbol('rcsb', 'validation-report.issue-count', Type.Num),
  123. ctx => {
  124. const { unit, element } = ctx.element
  125. if (!Unit.isAtomic(unit)) return 0
  126. const validationReport = ValidationReportProvider.get(unit.model).value
  127. return validationReport?.geometryIssues.get(unit.residueIndex[element])?.size || 0
  128. }
  129. ),
  130. }
  131. }
  132. const FileSourceParams = {
  133. input: PD.File({ accept: '.xml,.gz,.zip' })
  134. }
  135. type FileSourceProps = PD.Values<typeof FileSourceParams>
  136. const ServerSourceParams = {
  137. baseUrl: PD.Text(ValidationReport.DefaultBaseUrl, { description: 'Base URL to directory tree' })
  138. }
  139. type ServerSourceProps = PD.Values<typeof ServerSourceParams>
  140. export const ValidationReportParams = {
  141. source: PD.MappedStatic('server', {
  142. 'file': PD.Group(FileSourceParams, { label: 'File', isFlat: true }),
  143. 'server': PD.Group(ServerSourceParams, { label: 'Server', isFlat: true }),
  144. }, { options: [['file', 'File'], ['server', 'Server']] })
  145. }
  146. export type ValidationReportParams = typeof ValidationReportParams
  147. export type ValidationReportProps = PD.Values<ValidationReportParams>
  148. export const ValidationReportProvider: CustomModelProperty.Provider<ValidationReportParams, ValidationReport> = CustomModelProperty.createProvider({
  149. label: 'Validation Report',
  150. descriptor: CustomPropertyDescriptor({
  151. name: 'rcsb_validation_report',
  152. symbols: ValidationReport.symbols
  153. }),
  154. type: 'dynamic',
  155. defaultParams: ValidationReportParams,
  156. getParams: (data: Model) => ValidationReportParams,
  157. isApplicable: (data: Model) => ValidationReport.isApplicable(data),
  158. obtain: async (ctx: CustomProperty.Context, data: Model, props: Partial<ValidationReportProps>) => {
  159. const p = { ...PD.getDefaultValues(ValidationReportParams), ...props }
  160. return await ValidationReport.obtain(ctx, data, p)
  161. }
  162. })
  163. //
  164. type IntraUnitClashesProps = {
  165. readonly id: ArrayLike<number>
  166. readonly magnitude: ArrayLike<number>
  167. readonly distance: ArrayLike<number>
  168. }
  169. type InterUnitClashesProps = {
  170. readonly id: number
  171. readonly magnitude: number
  172. readonly distance: number
  173. }
  174. export type IntraUnitClashes = IntAdjacencyGraph<UnitIndex, IntraUnitClashesProps>
  175. export type InterUnitClashes = InterUnitGraph<Unit.Atomic, UnitIndex, InterUnitClashesProps>
  176. export interface Clashes {
  177. readonly interUnit: InterUnitClashes
  178. readonly intraUnit: IntMap<IntraUnitClashes>
  179. }
  180. function createInterUnitClashes(structure: Structure, clashes: ValidationReport['clashes']) {
  181. const builder = new InterUnitGraph.Builder<Unit.Atomic, UnitIndex, InterUnitClashesProps>()
  182. const { a, b, edgeProps: { id, magnitude, distance } } = clashes
  183. const pA = Vec3(), pB = Vec3()
  184. Structure.eachUnitPair(structure, (unitA: Unit, unitB: Unit) => {
  185. const elementsA = unitA.elements
  186. const elementsB = unitB.elements
  187. builder.startUnitPair(unitA as Unit.Atomic, unitB as Unit.Atomic)
  188. for (let i = 0, il = clashes.edgeCount * 2; i < il; ++i) {
  189. // TODO create lookup
  190. let indexA = SortedArray.indexOf(elementsA, a[i])
  191. let indexB = SortedArray.indexOf(elementsB, b[i])
  192. if (indexA !== -1 && indexB !== -1) {
  193. unitA.conformation.position(a[i], pA)
  194. unitB.conformation.position(b[i], pB)
  195. // check actual distance to avoid clashes between unrelated chain instances
  196. if (equalEps(distance[i], Vec3.distance(pA, pB), 0.1)) {
  197. builder.add(indexA as UnitIndex, indexB as UnitIndex, {
  198. id: id[i],
  199. magnitude: magnitude[i],
  200. distance: distance[i],
  201. })
  202. }
  203. }
  204. }
  205. builder.finishUnitPair()
  206. }, {
  207. maxRadius: arrayMax(clashes.edgeProps.distance),
  208. validUnit: (unit: Unit) => Unit.isAtomic(unit),
  209. validUnitPair: (unitA: Unit, unitB: Unit) => unitA.model === unitB.model
  210. })
  211. return new InterUnitGraph(builder.getMap())
  212. }
  213. function createIntraUnitClashes(unit: Unit.Atomic, clashes: ValidationReport['clashes']): IntraUnitClashes {
  214. const aIndices: UnitIndex[] = []
  215. const bIndices: UnitIndex[] = []
  216. const ids: number[] = []
  217. const magnitudes: number[] = []
  218. const distances: number[] = []
  219. const pA = Vec3(), pB = Vec3()
  220. const { elements } = unit
  221. const { a, b, edgeCount, edgeProps } = clashes
  222. for (let i = 0, il = edgeCount * 2; i < il; ++i) {
  223. // TODO create lookup
  224. let indexA = SortedArray.indexOf(elements, a[i])
  225. let indexB = SortedArray.indexOf(elements, b[i])
  226. if (indexA !== -1 && indexB !== -1) {
  227. unit.conformation.position(a[i], pA)
  228. unit.conformation.position(b[i], pB)
  229. // check actual distance to avoid clashes between unrelated chain instances
  230. if (equalEps(edgeProps.distance[i], Vec3.distance(pA, pB), 0.1)) {
  231. aIndices.push(indexA as UnitIndex)
  232. bIndices.push(indexB as UnitIndex)
  233. ids.push(edgeProps.id[i])
  234. magnitudes.push(edgeProps.magnitude[i])
  235. distances.push(edgeProps.distance[i])
  236. }
  237. }
  238. }
  239. const builder = new IntAdjacencyGraph.EdgeBuilder(elements.length, aIndices, bIndices)
  240. const id = new Int32Array(builder.slotCount)
  241. const magnitude = new Float32Array(builder.slotCount)
  242. const distance = new Float32Array(builder.slotCount)
  243. for (let i = 0, _i = builder.edgeCount; i < _i; i++) {
  244. builder.addNextEdge()
  245. builder.assignProperty(id, ids[i])
  246. builder.assignProperty(magnitude, magnitudes[i])
  247. builder.assignProperty(distance, distances[i])
  248. }
  249. return builder.createGraph({ id, magnitude, distance })
  250. }
  251. function createClashes(structure: Structure, clashes: ValidationReport['clashes']): Clashes {
  252. const intraUnit = IntMap.Mutable<IntraUnitClashes>()
  253. for (let i = 0, il = structure.unitSymmetryGroups.length; i < il; ++i) {
  254. const group = structure.unitSymmetryGroups[i]
  255. if (!Unit.isAtomic(group.units[0])) continue
  256. const intraClashes = createIntraUnitClashes(group.units[0], clashes)
  257. for (let j = 0, jl = group.units.length; j < jl; ++j) {
  258. intraUnit.set(group.units[j].id, intraClashes)
  259. }
  260. }
  261. return {
  262. interUnit: createInterUnitClashes(structure, clashes),
  263. intraUnit
  264. }
  265. }
  266. export const ClashesProvider: CustomStructureProperty.Provider<{}, Clashes> = CustomStructureProperty.createProvider({
  267. label: 'Clashes',
  268. descriptor: CustomPropertyDescriptor({
  269. name: 'rcsb_clashes',
  270. // TODO `cifExport` and `symbol`
  271. }),
  272. type: 'local',
  273. defaultParams: {},
  274. getParams: (data: Structure) => ({}),
  275. isApplicable: (data: Structure) => true,
  276. obtain: async (ctx: CustomProperty.Context, data: Structure) => {
  277. await ValidationReportProvider.attach(ctx, data.models[0])
  278. const validationReport = ValidationReportProvider.get(data.models[0]).value!
  279. return createClashes(data, validationReport.clashes)
  280. }
  281. })
  282. //
  283. function getItem(a: NamedNodeMap, name: string) {
  284. const item = a.getNamedItem(name)
  285. return item !== null ? item.value : ''
  286. }
  287. function hasAttr(a: NamedNodeMap, name: string, value: string) {
  288. const item = a.getNamedItem(name)
  289. return item !== null && item.value === value
  290. }
  291. function getMogInfo(a: NamedNodeMap) {
  292. return {
  293. mean: parseFloat(getItem(a, 'mean')),
  294. obs: parseFloat(getItem(a, 'obsval')),
  295. stdev: parseFloat(getItem(a, 'stdev')),
  296. z: parseFloat(getItem(a, 'Zscore')),
  297. }
  298. }
  299. function getMolInfo(a: NamedNodeMap) {
  300. return {
  301. mean: parseFloat(getItem(a, 'mean')),
  302. obs: parseFloat(getItem(a, 'obs')),
  303. stdev: parseFloat(getItem(a, 'stdev')),
  304. z: parseInt(getItem(a, 'z')),
  305. }
  306. }
  307. function addIndex(index: number, element: ElementIndex, map: Map<ElementIndex, number[]>) {
  308. if (map.has(element)) map.get(element)!.push(index)
  309. else map.set(element, [index])
  310. }
  311. function ClashesBuilder(elementsCount: number) {
  312. const aIndices: ElementIndex[] = []
  313. const bIndices: ElementIndex[] = []
  314. const ids: number[] = []
  315. const magnitudes: number[] = []
  316. const distances: number[] = []
  317. const seen = new Map<string, ElementIndex>()
  318. return {
  319. add(element: ElementIndex, id: number, magnitude: number, distance: number, isSymop: boolean) {
  320. const hash = `${id}|${isSymop ? 's' : ''}`
  321. const other = seen.get(hash)
  322. if (other !== undefined) {
  323. aIndices[aIndices.length] = element
  324. bIndices[bIndices.length] = other
  325. ids[ids.length] = id
  326. magnitudes[magnitudes.length] = magnitude
  327. distances[distances.length] = distance
  328. } else {
  329. seen.set(hash, element)
  330. }
  331. },
  332. get() {
  333. const builder = new IntAdjacencyGraph.EdgeBuilder(elementsCount, aIndices, bIndices)
  334. const id = new Int32Array(builder.slotCount)
  335. const magnitude = new Float32Array(builder.slotCount)
  336. const distance = new Float32Array(builder.slotCount)
  337. for (let i = 0, _i = builder.edgeCount; i < _i; i++) {
  338. builder.addNextEdge()
  339. builder.assignProperty(id, ids[i])
  340. builder.assignProperty(magnitude, magnitudes[i])
  341. builder.assignProperty(distance, distances[i])
  342. }
  343. return builder.createGraph({ id, magnitude, distance })
  344. }
  345. }
  346. }
  347. function parseValidationReportXml(xml: XMLDocument, model: Model): ValidationReport {
  348. const rsrz = new Map<ResidueIndex, number>()
  349. const rscc = new Map<ResidueIndex, number>()
  350. const rci = new Map<ResidueIndex, number>()
  351. const geometryIssues = new Map<ResidueIndex, Set<string>>()
  352. const bondOutliers = {
  353. index: new Map<ElementIndex, number[]>(),
  354. data: [] as ValidationReport['bondOutliers']['data']
  355. }
  356. const angleOutliers = {
  357. index: new Map<ElementIndex, number[]>(),
  358. data: [] as ValidationReport['angleOutliers']['data']
  359. }
  360. const clashesBuilder = ClashesBuilder(model.atomicHierarchy.atoms._rowCount)
  361. const { index } = model.atomicHierarchy
  362. const entries = xml.getElementsByTagName('Entry')
  363. if (entries.length === 1) {
  364. const chemicalShiftLists = entries[0].getElementsByTagName('chemical_shift_list')
  365. if (chemicalShiftLists.length === 1) {
  366. const randomCoilIndices = chemicalShiftLists[0].getElementsByTagName('random_coil_index')
  367. for (let j = 0, jl = randomCoilIndices.length; j < jl; ++j) {
  368. const { attributes } = randomCoilIndices[j]
  369. const value = parseFloat(getItem(attributes, 'value'))
  370. const auth_asym_id = getItem(attributes, 'chain')
  371. const auth_comp_id = getItem(attributes, 'rescode')
  372. const auth_seq_id = parseInt(getItem(attributes, 'resnum'))
  373. const rI = index.findResidueAuth({ auth_asym_id, auth_comp_id, auth_seq_id })
  374. if (rI !== -1) rci.set(rI, value)
  375. }
  376. }
  377. }
  378. const groups = xml.getElementsByTagName('ModelledSubgroup')
  379. for (let i = 0, il = groups.length; i < il; ++i) {
  380. const g = groups[ i ]
  381. const ga = g.attributes
  382. const pdbx_PDB_model_num = parseInt(getItem(ga, 'model'))
  383. if (model.modelNum !== pdbx_PDB_model_num) continue
  384. const auth_asym_id = getItem(ga, 'chain')
  385. const auth_comp_id = getItem(ga, 'resname')
  386. const auth_seq_id = parseInt(getItem(ga, 'resnum'))
  387. const pdbx_PDB_ins_code = getItem(ga, 'icode').trim() || undefined
  388. const label_alt_id = getItem(ga, 'altcode').trim() || undefined
  389. const rI = index.findResidueAuth({ auth_asym_id, auth_comp_id, auth_seq_id, pdbx_PDB_ins_code })
  390. // continue if no residue index is found
  391. if (rI === -1) continue
  392. if (ga.getNamedItem('rsrz') !== null) rsrz.set(rI, parseFloat(getItem(ga, 'rsrz')))
  393. if (ga.getNamedItem('rscc') !== null) rscc.set(rI, parseFloat(getItem(ga, 'rscc')))
  394. const isPolymer = getItem(ga, 'seq') !== '.'
  395. const issues = new Set<string>()
  396. if (isPolymer) {
  397. const molBondOutliers = g.getElementsByTagName('bond-outlier')
  398. if (molBondOutliers.length) issues.add('bond-outlier')
  399. for (let j = 0, jl = molBondOutliers.length; j < jl; ++j) {
  400. const bo = molBondOutliers[j].attributes
  401. const idx = bondOutliers.data.length
  402. const atomA = index.findAtomOnResidue(rI, getItem(bo, 'atom0'))
  403. const atomB = index.findAtomOnResidue(rI, getItem(bo, 'atom1'))
  404. addIndex(idx, atomA, bondOutliers.index)
  405. addIndex(idx, atomB, bondOutliers.index)
  406. bondOutliers.data.push({
  407. tag: 'bond-outlier', atomA, atomB, ...getMolInfo(bo)
  408. })
  409. }
  410. const molAngleOutliers = g.getElementsByTagName('angle-outlier')
  411. if (molAngleOutliers.length) issues.add('angle-outlier')
  412. for (let j = 0, jl = molAngleOutliers.length; j < jl; ++j) {
  413. const ao = molAngleOutliers[j].attributes
  414. const idx = bondOutliers.data.length
  415. const atomA = index.findAtomOnResidue(rI, getItem(ao, 'atom0'))
  416. const atomB = index.findAtomOnResidue(rI, getItem(ao, 'atom1'))
  417. const atomC = index.findAtomOnResidue(rI, getItem(ao, 'atom2'))
  418. addIndex(idx, atomA, angleOutliers.index)
  419. addIndex(idx, atomB, angleOutliers.index)
  420. addIndex(idx, atomC, angleOutliers.index)
  421. angleOutliers.data.push({
  422. tag: 'angle-outlier', atomA, atomB, atomC, ...getMolInfo(ao)
  423. })
  424. }
  425. const planeOutliers = g.getElementsByTagName('plane-outlier')
  426. if (planeOutliers.length) issues.add('plane-outlier')
  427. if (hasAttr(ga, 'rota', 'OUTLIER')) issues.add('rotamer-outlier')
  428. if (hasAttr(ga, 'rama', 'OUTLIER')) issues.add('ramachandran-outlier')
  429. if (hasAttr(ga, 'RNApucker', 'outlier')) issues.add('RNApucker-outlier')
  430. } else {
  431. const mogBondOutliers = g.getElementsByTagName('mog-bond-outlier')
  432. if (mogBondOutliers.length) issues.add('mog-bond-outlier')
  433. for (let j = 0, jl = mogBondOutliers.length; j < jl; ++j) {
  434. const mbo = mogBondOutliers[j].attributes
  435. const atoms = getItem(mbo, 'atoms').split(',')
  436. const idx = bondOutliers.data.length
  437. const atomA = index.findAtomOnResidue(rI, atoms[0])
  438. const atomB = index.findAtomOnResidue(rI, atoms[1])
  439. addIndex(idx, atomA, bondOutliers.index)
  440. addIndex(idx, atomB, bondOutliers.index)
  441. bondOutliers.data.push({
  442. tag: 'mog-bond-outlier', atomA, atomB, ...getMogInfo(mbo)
  443. })
  444. }
  445. const mogAngleOutliers = g.getElementsByTagName('mog-angle-outlier')
  446. if (mogAngleOutliers.length) issues.add('mog-angle-outlier')
  447. for (let j = 0, jl = mogAngleOutliers.length; j < jl; ++j) {
  448. const mao = mogAngleOutliers[j].attributes
  449. const atoms = getItem(mao, 'atoms').split(',')
  450. const idx = angleOutliers.data.length
  451. const atomA = index.findAtomOnResidue(rI, atoms[0])
  452. const atomB = index.findAtomOnResidue(rI, atoms[1])
  453. const atomC = index.findAtomOnResidue(rI, atoms[2])
  454. addIndex(idx, atomA, angleOutliers.index)
  455. addIndex(idx, atomB, angleOutliers.index)
  456. addIndex(idx, atomC, angleOutliers.index)
  457. angleOutliers.data.push({
  458. tag: 'mog-angle-outlier', atomA, atomB, atomC, ...getMogInfo(mao)
  459. })
  460. }
  461. }
  462. const clashes = g.getElementsByTagName('clash')
  463. if (clashes.length) issues.add('clash')
  464. for (let j = 0, jl = clashes.length; j < jl; ++j) {
  465. const ca = clashes[j].attributes
  466. const id = parseInt(getItem(ca, 'cid'))
  467. const magnitude = parseFloat(getItem(ca, 'clashmag'))
  468. const distance = parseFloat(getItem(ca, 'dist'))
  469. const label_atom_id = getItem(ca, 'atom')
  470. const element = index.findAtomOnResidue(rI, label_atom_id, label_alt_id)
  471. if (element !== -1) {
  472. clashesBuilder.add(element, id, magnitude, distance, false)
  473. }
  474. }
  475. const symmClashes = g.getElementsByTagName('symm-clash')
  476. if (symmClashes.length) issues.add('symm-clash')
  477. for (let j = 0, jl = symmClashes.length; j < jl; ++j) {
  478. const sca = symmClashes[j].attributes
  479. const id = parseInt(getItem(sca, 'scid'))
  480. const magnitude = parseFloat(getItem(sca, 'clashmag'))
  481. const distance = parseFloat(getItem(sca, 'dist'))
  482. const label_atom_id = getItem(sca, 'atom')
  483. const element = index.findAtomOnResidue(rI, label_atom_id, label_alt_id)
  484. if (element !== -1) {
  485. clashesBuilder.add(element, id, magnitude, distance, true)
  486. }
  487. }
  488. geometryIssues.set(rI, issues)
  489. }
  490. const clashes = clashesBuilder.get()
  491. const validationReport = {
  492. rsrz, rscc, rci, geometryIssues,
  493. bondOutliers, angleOutliers,
  494. clashes
  495. }
  496. return validationReport
  497. }