-
Notifications
You must be signed in to change notification settings - Fork 266
ENH: Set symmetric threshold for identifying unit quaternions in qform calculation #1182
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
41ce88c
TEST: Check that quaternions.fillpositive does not augment unit vectors
effigies 943c13d
ENH: Set symmetric threshold for identifying unit quaternions in qfor…
effigies 0ecaa8e
DOC: Update signs in qform result to satisfy doctests
effigies 3f30ab5
ENH: Set w2_thresh to positive values for clarity, update doc to indi…
effigies 6b9b676
STY: Use norm(), matmul and list comprehensions
effigies aa4b017
TEST: Check case that exceeds threshold
effigies File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,35 +16,40 @@ | |
from .. import eulerangles as nea | ||
from .. import quaternions as nq | ||
|
||
|
||
def norm(vec): | ||
# Return unit vector with same orientation as input vector | ||
return vec / np.sqrt(vec @ vec) | ||
|
||
|
||
def gen_vec(dtype): | ||
# Generate random 3-vector in [-1, 1]^3 | ||
rand = np.random.default_rng() | ||
return rand.uniform(low=-1.0, high=1.0, size=(3,)).astype(dtype) | ||
|
||
|
||
# Example rotations | ||
eg_rots = [] | ||
params = (-pi, pi, pi / 2) | ||
zs = np.arange(*params) | ||
ys = np.arange(*params) | ||
xs = np.arange(*params) | ||
for z in zs: | ||
for y in ys: | ||
for x in xs: | ||
eg_rots.append(nea.euler2mat(z, y, x)) | ||
eg_rots = [ | ||
nea.euler2mat(z, y, x) | ||
for z in np.arange(-pi, pi, pi / 2) | ||
for y in np.arange(-pi, pi, pi / 2) | ||
for x in np.arange(-pi, pi, pi / 2) | ||
] | ||
|
||
# Example quaternions (from rotations) | ||
eg_quats = [] | ||
for M in eg_rots: | ||
eg_quats.append(nq.mat2quat(M)) | ||
eg_quats = [nq.mat2quat(M) for M in eg_rots] | ||
# M, quaternion pairs | ||
eg_pairs = list(zip(eg_rots, eg_quats)) | ||
|
||
# Set of arbitrary unit quaternions | ||
unit_quats = set() | ||
params = range(-2, 3) | ||
for w in params: | ||
for x in params: | ||
for y in params: | ||
for z in params: | ||
q = (w, x, y, z) | ||
Nq = np.sqrt(np.dot(q, q)) | ||
if not Nq == 0: | ||
q = tuple([e / Nq for e in q]) | ||
unit_quats.add(q) | ||
unit_quats = set( | ||
tuple(norm(np.r_[w, x, y, z])) | ||
for w in range(-2, 3) | ||
for x in range(-2, 3) | ||
for y in range(-2, 3) | ||
for z in range(-2, 3) | ||
if (w, x, y, z) != (0, 0, 0, 0) | ||
) | ||
|
||
|
||
def test_fillpos(): | ||
|
@@ -69,6 +74,51 @@ def test_fillpos(): | |
assert wxyz[0] == 0.0 | ||
|
||
|
||
@pytest.mark.parametrize('dtype', ('f4', 'f8')) | ||
def test_fillpositive_plus_minus_epsilon(dtype): | ||
# Deterministic test for fillpositive threshold | ||
# We are trying to fill (x, y, z) with a w such that |(w, x, y, z)| == 1 | ||
# If |(x, y, z)| is slightly off one, w should still be 0 | ||
nptype = np.dtype(dtype).type | ||
|
||
# Obviously, |(x, y, z)| == 1 | ||
baseline = np.array([0, 0, 1], dtype=dtype) | ||
|
||
# Obviously, |(x, y, z)| ~ 1 | ||
plus = baseline * nptype(1 + np.finfo(dtype).eps) | ||
minus = baseline * nptype(1 - np.finfo(dtype).eps) | ||
|
||
assert nq.fillpositive(plus)[0] == 0.0 | ||
assert nq.fillpositive(minus)[0] == 0.0 | ||
|
||
# |(x, y, z)| > 1, no real solutions | ||
plus = baseline * nptype(1 + 2 * np.finfo(dtype).eps) | ||
with pytest.raises(ValueError): | ||
nq.fillpositive(plus) | ||
|
||
# |(x, y, z)| < 1, two real solutions, we choose positive | ||
minus = baseline * nptype(1 - 2 * np.finfo(dtype).eps) | ||
assert nq.fillpositive(minus)[0] > 0.0 | ||
|
||
|
||
@pytest.mark.parametrize('dtype', ('f4', 'f8')) | ||
def test_fillpositive_simulated_error(dtype): | ||
# Nondeterministic test for fillpositive threshold | ||
# Create random vectors, normalize to unit length, and count on floating point | ||
# error to result in magnitudes larger/smaller than one | ||
# This is to simulate cases where a unit quaternion with w == 0 would be encoded | ||
# as xyz with small error, and we want to recover the w of 0 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I still like this as a bit of a stress test that will hit more realistic rounding errors, and added a comment to explain more. Can drop it if it seems excessive. |
||
|
||
# Permit 1 epsilon per value (default, but make explicit here) | ||
w2_thresh = 3 * np.finfo(dtype).eps | ||
|
||
pos_error = neg_error = False | ||
for _ in range(50): | ||
xyz = norm(gen_vec(dtype)) | ||
|
||
assert nq.fillpositive(xyz, w2_thresh)[0] == 0.0 | ||
|
||
|
||
def test_conjugate(): | ||
# Takes sequence | ||
cq = nq.conjugate((1, 0, 0, 0)) | ||
|
@@ -125,7 +175,7 @@ def test_norm(): | |
def test_mult(M1, q1, M2, q2): | ||
# Test that quaternion * same as matrix * | ||
q21 = nq.mult(q2, q1) | ||
assert_array_almost_equal, np.dot(M2, M1), nq.quat2mat(q21) | ||
assert_array_almost_equal, M2 @ M1, nq.quat2mat(q21) | ||
|
||
|
||
@pytest.mark.parametrize('M, q', eg_pairs) | ||
|
@@ -146,7 +196,7 @@ def test_eye(): | |
@pytest.mark.parametrize('M, q', eg_pairs) | ||
def test_qrotate(vec, M, q): | ||
vdash = nq.rotate_vector(vec, q) | ||
vM = np.dot(M, vec) | ||
vM = M @ vec | ||
assert_array_almost_equal(vdash, vM) | ||
|
||
|
||
|
@@ -179,6 +229,6 @@ def test_angle_axis(): | |
nq.nearly_equivalent(q, q2) | ||
aa_mat = nq.angle_axis2mat(theta, vec) | ||
assert_array_almost_equal(aa_mat, M) | ||
unit_vec = vec / np.sqrt(vec.dot(vec)) | ||
unit_vec = norm(vec) | ||
aa_mat2 = nq.angle_axis2mat(theta, unit_vec, is_normalized=True) | ||
assert_array_almost_equal(aa_mat2, M) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@matthew-brett How's this for a deterministic test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice - but - consider pusing to threshold to show where it fails?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. It actually fails at 2*eps, since the error compounds. Updated.