#2758 closed defect (fixed)
Place.rotation_axis_and_angle not robust
Reported by: | Tristan Croll | Owned by: | Tom Goddard |
---|---|---|---|
Priority: | normal | Milestone: | |
Component: | Core | Version: | |
Keywords: | Cc: | ||
Blocked By: | Blocking: | ||
Notify when closed: | Platform: | all | |
Project: | ChimeraX |
Description
The following bug report has been submitted: Platform: Linux-3.10.0-1062.9.1.el7.x86_64-x86_64-with-centos-7.7.1908-Core ChimeraX Version: 0.92 (2020-01-11) Description Place.rotation_axis_and_angle() returns incorrect values when the rotation matrix is symmetric or nearly so. Following the link to http://en.wikipedia.org/wiki/Rotation_matrix from core.geometry.matrix.R_to_axis_angle(): "This does not work if R is symmetric. Above, if R − R T is zero, then all subsequent steps are invalid. In this case, it is necessary to diagonalize and find the eigenvector corresponding to an eigenvalue of 1. " Simple case of a flip about x: {{{ p = Place(axes=[[1,0,0],[0,-1,0],[0,0,-1]]) p.rotation_axis_and_angle() (array([0., 0., 1.]), 0) # should be [1,0,0],180 # A more "real" case with some numeric noise p = Place(axes=[[1,0,0],[-1e-16,-1,0],[-1e-16,0,-1]]) p.rotation_axis_and_angle() (array([ 0. , -0.70710678, 0.70710678]), 180.0) }}} Log: UCSF ChimeraX version: 0.92 (2020-01-11) © 2016-2019 Regents of the University of California. All rights reserved. How to cite UCSF ChimeraX > open 6ig2 structureFactors true Summary of feedback from opening 6ig2 fetched from pdb --- warning | WARNING: multiple experimental reflection datasets found: F_meas_au, F_meas_sigma_au, intensity_meas, intensity_sigma Automatically choosing "intensity_meas, intensity_sigma". notes | Fetching compressed mmCIF 6ig2 from http://files.rcsb.org/download/6ig2.cif Fetching CCD CTP from http://ligand-expo.rcsb.org/reports/C/CTP/CTP.cif Fetching compressed 6ig2 structure factors from http://files.rcsb.org/download/6ig2-sf.cif Resolution: 2.8817619976202815 Reflection data provided as intensities. Performing French & Wilson scaling to convert to amplitudes... 6ig2 title: Structure of mitochondrial CDP-DAG synthase Tam41 complexed with CTP, δ 74, F240A [more info...] Chain information for 6ig2 --- Chain | Description 1.1/A 1.1/B 1.1/C 1.1/D | Phosphatidate cytidylyltransferase, mitochondrial Non-standard residues in 6ig2 #1.1 --- CTP — cytidine-5'-triphosphate 6ig2 mmCIF Assemblies --- 1| author_and_software_defined_assembly 2| author_and_software_defined_assembly > toolshed show Shell /opt/UCSF/ChimeraX-daily/lib/python3.7/site- packages/IPython/core/history.py:226: UserWarning: IPython History requires SQLite, your history will not be saved warn("IPython History requires SQLite, your history will not be saved") OpenGL version: 3.3.0 NVIDIA 440.33.01 OpenGL renderer: TITAN Xp/PCIe/SSE2 OpenGL vendor: NVIDIA Corporation
Change History (12)
follow-up: 2 comment:2 by , 6 years ago
Ahh... I see this is a more complex task than I thought. The eig() function sometimes returns real arrays, sometimes complex... but more importantly this approach doesn't determine the sign of the rotation. Also turns out that SciPy has it covered: {{{ def rot33_as_axis_and_angle(rot33): from scipy.spatial.transform import Rotation as R r = R.from_dcm(rot33) from math import degrees rvec = r.as_rotvec() rnorm = numpy.linalg.norm(rvec) angle = degrees(rnorm) return (rvec/rnorm, angle) }}} ... albeit a bit slowly (270 us). Plenty fast enough for my purpose, but might be a bit on the slow side if this is used anywhere in big loops? For what it's worth, `numpy.allclose(rot33, rot33.T)` costs about 65 us - so where it really matters you could use that to decide whether to use the fast or the robust method. On 2020-01-17 11:06, ChimeraX wrote:
comment:3 by , 6 years ago
Component: | Unassigned → Core |
---|---|
Owner: | set to |
Platform: | → all |
Project: | → ChimeraX |
Status: | new → assigned |
Summary: | ChimeraX bug report submission → Place.rotation_axis_and_angle not robust |
comment:4 by , 6 years ago
Resolution: | → fixed |
---|---|
Status: | assigned → closed |
Fixed.
I put in special case code to handle the 180 degree rotation case (R = R_transpose).
comment:5 by , 6 years ago
The problem is for any 180 degree rotation (that is the R = Rt condition), and any round-off error leads to a pretty random axis, although the returned angle is 180. The 180 degree rotations are easy to extract the axis very fast from just the diagonal matrix elements
[[2ax2 - 1, 2ax*ay, 2ax*az],
[2*ax*ay, 2ay2-1, 2ay*az],
[2ax*az, 2ay*az, 2az2-1]]
but it could be a bit tricky to make the switch over from the non-180 to the 180 case not glitchy.
comment:6 by , 6 years ago
I guess the best approach would be to err on the side of caution, and make the switch when you still have at least a few significant figures to play with. Just messing around, a rotation of 180.0001 degrees about a random axis has differences between equivalent off-axis positions on the order of 1e-6, and the existing code still nails it at that point. Given that's about the limit of float32 precision anyway, why not put the cutoff somewhere around there?
follow-up: 7 comment:7 by , 6 years ago
Double precision (float64) is being used for the 3x4 transformation matrices. I made the cutoff 1e-14 (rotation matrix elements are less than 1). I did not study it carefully. If you run experiments with the current code that suggests a bigger cutoff is better let me know the results.
comment:8 by , 6 years ago
OK. A quick run with the following snippet:
def test_rotation_accuracy(): import numpy from chimerax.core.geometry import rotation from collections import defaultdict axis_errors = defaultdict(lambda: list()) angle_errors = defaultdict(lambda: list()) for i in range(20): angle = 180-10**-i for j in range(50): axis = numpy.random.rand(3)-0.5 axis/=numpy.linalg.norm(axis) r = rotation(axis, angle) calc_axis, calc_angle = r.rotation_axis_and_angle() if sum(calc_axis - axis) > 0.1: calc_axis = -calc_axis calc_angle = 360-calc_angle axis_errors[i].append(numpy.max(numpy.abs(calc_axis-axis))) angle_errors[i].append(abs(calc_angle-angle)) return dict(axis_errors), dict(angle_errors)
... says that the absolute error along any given axis dimension passes 1e-6 at about i=9 (that is, where the angle is 180-(1e-9) degrees), at which point the off-axis elements differ by about 1e-10. So that seems like a reasonable point. The error hits around 1e-3 for i=12, and by i=13 the result is pretty much random.
comment:9 by , 6 years ago
Ok I changed the cutoff to 1e-9 after some testing. My previous code for near 180 degrees was simply wrong, had wrong signs on the axis components (always made them positive). And your test was also a bit wrong, the sum(calc_axis - axis) needed to be sum(abs(calc_axis - axis)).
Axis and angle accuracies near 1e-8 aren't to great given float64 is in use, but it will do.
follow-up: 10 comment:10 by , 6 years ago
Still one more bug, I'm afraid: p = Place() p.rotation_axis_and_angle() Out[22]: (array([1., 1., 1.]), 180) On 2020-01-28 20:36, ChimeraX wrote:
comment:12 by , 6 years ago
Fixed.
The identity rotation is symmetric and was incorrectly being handled by the 180 degree rotation code which was just looking for symmetric matrices.