Skip to content

Conversation

mayankray2020
Copy link
Contributor

References to other Issues or PRs

Fixes #20769

Brief description of what is fixed or changed

Added the features mentioned in #20769

  • algebras
    • quaternion

@sympy-bot
Copy link

sympy-bot commented Jan 23, 2021

Hi, I am the SymPy bot (v161). I'm here to help you write a release notes entry. Please read the guide on how to write release notes.

Your release notes are in good order.

Here is what the release notes will look like:

This will be added to https://github.com/sympy/sympy/wiki/Release-Notes-for-1.9.

Click here to see the pull request description that was parsed.
<!-- Your title above should be a short description of what
was changed. Do not include the issue number in the title. -->

#### References to other Issues or PRs
<!-- If this pull request fixes an issue, write "Fixes #NNNN" in that exact
format, e.g. "Fixes #1234" (see
https://tinyurl.com/auto-closing for more information). Also, please
write a comment on that issue linking back to this pull request once it is
open. -->
Fixes #20769 


#### Brief description of what is fixed or changed
Added the features mentioned in #20769

<!-- BEGIN RELEASE NOTES -->
* algebras
  * quaternion
<!-- END RELEASE NOTES -->

@mayankray2020
Copy link
Contributor Author

@oscarbenjamin please review

@oscarbenjamin
Copy link
Collaborator

@danilobellini can you review this?

Copy link

@danilobellini danilobellini left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be more consistent if the behavior of q0.is_pure() would be the same of the zero is_imaginary(), that is, zero itself would neither be seen a imaginary nor as a pure quaternion (it's the only one with real part zero that wouldn't be called "pure"). That is, is_pure should check something like self.a != 0 and self != 0 (but using self.b, self.c and self.d instead in the second comparison).

Other checks with zero (like coplanarity) are indeed difficult details. The plane of a quaternion is orthogonal to its axis, and the axis of zero is undefined, therefore the binary coplanarity check should probably also be seen as undefined, I don't know if raising ValueError or returning nan would be better. At first, probably a ValueError makes more sense for the binary quaternion coplanarity check, and I would say the same if the input quaternion has an undefined axis (that is, if b, c and d are all zero for either self or other, self.coplanar(other) should raise an error).

However, for the ternary coplanarity check, the origin is in every plane, it would make more sense if every three quaternions in such a check would be said coplanar. Actually, that's the modern coplanarity checking through the rank of the matrix of the coordinates. The current ternary_coplanar() implementation isn't checking coplanarity and really need changes (binary and ternary coplanarity are really different operations). For the ternary, just check if the determinant of [[self.b, self.c, self.d], [q1.b, q1.c, q1.d], [q2.b, q2.c, q2.d]] is zero: if it is, then the three are coplanar. Using just quaternions operations to check that is possible (self.orthogonal(q1/q2) or any other combination if the division is undefined), but it's the same.

I would change some of the asserts due to this "zero" behavior, and perhaps add some other checks because of the ternary coplanarity check.

The implementation of some of these could had been more straightforward, is there any reason to store everything into local variables before returning, e.g. return self.a?

Some other details would help in the explanation of the docstrings, e.g. "pure quaternions seen as 3D vectors" instead of just "pure quaternions". There should be an explicit description that "coplanar()" checks the plane of the quaternion (which is orthogonal to its axis) whereas the "ternary_coplanar" checks the axis of the quaternions (i.e., the quaternions seen as 3D vectors). The axis is "the versor of the vector part of the quaternion", and the quaternion angle is the one "measured in the real-axis plane", these two are probably known by anyone who studies quaternions in the classical sense, but it's worth adding a little bit more text.

@mayankray2020
Copy link
Contributor Author

@danilobellini please review

@mayankray2020
Copy link
Contributor Author

@danilobellini @oscarbenjamin I've made the suggested changes. Please review it.

Copy link

@danilobellini danilobellini left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need to replace everything just to use is_pure instead of checking if self.a == 0 or something alike. The zero quaternion is orthogonal and parallel to every other quaternion at the same time, and the "ternary coplanar" (or axis coplanar) also makes sense for the zero quaternion (from the description of my previous review). There are only two cases where the quaternion zero should be really handled: (1) for the is_pure, to tell that "zero" isn't a right/pure quaternion (it might be, but it's really undefined, and returning False makes sense, and the first updated assert is already verifying this), (2) the binary coplanarity (quaternion planes), which is undefined if the axis is undefined (so it should handle not just the zero but all zero-axis inputs).

In Quaternion.coplanar there are some double checks on a, and the and/or/and pattern looks confusing (which comes first?). Binary coplanarity is undefined if an input has an undefined axis, it makes sense to raise ValueError unless self.vector().is_pure() and the same for the otherinput.

The Quaternion.ternary_coplanar implementation, as well as orthogonal and parallel, might just check if a is zero, they still make sense otherwise (e.g. defining orthogonality as something like "dot product is zero" we would get the same result).

I'm not aware of how some internal automation of Sympy works, but I know sometimes I need to explicitly tell Sympy to expand/simplify/... an expression to get a straight result like "these two are indeed the same". I don't know if just checking (code) == 0 is better than code.simplify() == 0 or something alike using Eq or something else "internal". Even the or, when should it be used, and when the | operator should be used? For stuff like that, as well as code style, I think someone else should review.

@mayankray2020
Copy link
Contributor Author

There's no need to replace everything just to use is_pure instead of checking if self.a == 0 or something alike. The zero quaternion is orthogonal and parallel to every other quaternion at the same time, and the "ternary coplanar" (or axis coplanar) also makes sense for the zero quaternion (from the description of my previous review). There are only two cases where the quaternion zero should be really handled: (1) for the is_pure, to tell that "zero" isn't a right/pure quaternion (it might be, but it's really undefined, and returning False makes sense, and the first updated assert is already verifying this), (2) the binary coplanarity (quaternion planes), which is undefined if the axis is undefined (so it should handle not just the zero but all zero-axis inputs).

In Quaternion.coplanar there are some double checks on a, and the and/or/and pattern looks confusing (which comes first?). Binary coplanarity is undefined if an input has an undefined axis, it makes sense to raise ValueError unless self.vector().is_pure() and the same for the otherinput.

The Quaternion.ternary_coplanar implementation, as well as orthogonal and parallel, might just check if a is zero, they still make sense otherwise (e.g. defining orthogonality as something like "dot product is zero" we would get the same result).

I'm not aware of how some internal automation of Sympy works, but I know sometimes I need to explicitly tell Sympy to expand/simplify/... an expression to get a straight result like "these two are indeed the same". I don't know if just checking (code) == 0 is better than code.simplify() == 0 or something alike using Eq or something else "internal". Even the or, when should it be used, and when the | operator should be used? For stuff like that, as well as code style, I think someone else should review.

Actually in the definition Quaternion_ternanry_coplanar() it is said that it works for pure quaternions, but the 0 quaternion is not called pure so is that right if we allow Quaternion_ternanry_coplanar() to work for 0 'Quatertion'?

@danilobellini
Copy link

Actually in the definition Quaternion_ternanry_coplanar() it is said that it works for pure quaternions, but the 0 quaternion is not called pure so is that right if we allow Quaternion_ternanry_coplanar() to work for 0 'Quatertion'?

Since ternary_coplanar(), orthogonal() and parallel() are applied on "quaternions without real part seen as 3D vectors", zero makes some sense for them. Coplanarity means that the vectors (or the points and the origin) are in the same plane, that always happens for 3 vectors if one of them is zero. Orthogonality with that multiplication definition is like "dot product is zero", which makes zero orthogonal to all vectors. Parallelism defined as "one vector is a multiple of another" also makes zero parallel to every vector, and that's what happens with the multiplication definition.

By definition, Hamilton used the expression "right quaternion" to denote what we call "pure quaternion" nowadays. He uses "right" to mean that its angle is pi/2, something that happens for all quaternions whose real part is zero, but the zero quaternion, whose angle is undefined. However, "undefined" means neither "zero is a right quaternion" nor "zero isn't right quaternion", it just means that we don't know, it depends on something external (the context and/or how we use it).

When we are using quaternions as 3D vectors (with their real part equals to zero), our context makes zero a right/pure quaternion (one can think on it as a limit, where everything else in the domain has angle equals to pi/2).

When we are using unit quaternions (versors), they are like grand circle arcs in a specific plane but without a specific "position" in the circle, because we can interpret it as a rotation operator in that plane, multiplying it at left (that is, in OB/OA * OA we can regard the division OB/OA as the rotation quaternion that takes OA and "moves" it to OB, where O is the origin, A and B are points in the 3D unit sphere). In the multiplication we have two "natures" of quaternions: what matters for "OB/OA" is the quaternion plane (which contains OB and OA), for OA we don't have a real part (as it's a 3D vector). We can extend it to be not just a grand circle arc, but also a scale factor, an arc obtained with a linear interpolation of the angle and a logarithmic interpolation of the radius (like what happens with the multiplication of complex numbers). In this case, the magnitude of OA doesn't need to be 1. The "binary coplanar" is applied on quaternions like "OB/OA", whereas the "ternary coplanar" is applied on 3D vectors written as quaternions like "OA". If "OA" is zero, we won't be able to build a quaternion like "OB/OA", but "OA" is still interpreted as a 3D vector, hence its angle is still seen as pi/2 (due to the context). But if "OB" is zero, "OB/OA" still makes sense, but its angle is undefined, it's the angle from OA to OB in their plane, but we can't even define a single plane in this case, as happens when OA and OB are in the same line. In such case, coplanarity can't be evaluated between a quaternion and "OB/OA" since the quaternion doesn't know the direction of the 3D vectors OB and OA, and that's not just for the zero quaternion case but for all cases where the imaginary part is zero.

@mayankray2020
Copy link
Contributor Author

mayankray2020 commented Jan 30, 2021

Actually in the definition Quaternion_ternanry_coplanar() it is said that it works for pure quaternions, but the 0 quaternion is not called pure so is that right if we allow Quaternion_ternanry_coplanar() to work for 0 'Quatertion'?

Since ternary_coplanar(), orthogonal() and parallel() are applied on "quaternions without real part seen as 3D vectors", zero makes some sense for them. Coplanarity means that the vectors (or the points and the origin) are in the same plane, that always happens for 3 vectors if one of them is zero. Orthogonality with that multiplication definition is like "dot product is zero", which makes zero orthogonal to all vectors. Parallelism defined as "one vector is a multiple of another" also makes zero parallel to every vector, and that's what happens with the multiplication definition.

By definition, Hamilton used the expression "right quaternion" to denote what we call "pure quaternion" nowadays. He uses "right" to mean that its angle is pi/2, something that happens for all quaternions whose real part is zero, but the zero quaternion, whose angle is undefined. However, "undefined" means neither "zero is a right quaternion" nor "zero isn't right quaternion", it just means that we don't know, it depends on something external (the context and/or how we use it).

When we are using quaternions as 3D vectors (with their real part equals to zero), our context makes zero a right/pure quaternion (one can think on it as a limit, where everything else in the domain has angle equals to pi/2).

When we are using unit quaternions (versors), they are like grand circle arcs in a specific plane but without a specific "position" in the circle, because we can interpret it as a rotation operator in that plane, multiplying it at left (that is, in OB/OA * OA we can regard the division OB/OA as the rotation quaternion that takes OA and "moves" it to OB, where O is the origin, A and B are points in the 3D unit sphere). In the multiplication we have two "natures" of quaternions: what matters for "OB/OA" is the quaternion plane (which contains OB and OA), for OA we don't have a real part (as it's a 3D vector). We can extend it to be not just a grand circle arc, but also a scale factor, an arc obtained with a linear interpolation of the angle and a logarithmic interpolation of the radius (like what happens with the multiplication of complex numbers). In this case, the magnitude of OA doesn't need to be 1. The "binary coplanar" is applied on quaternions like "OB/OA", whereas the "ternary coplanar" is applied on 3D vectors written as quaternions like "OA". If "OA" is zero, we won't be able to build a quaternion like "OB/OA", but "OA" is still interpreted as a 3D vector, hence its angle is still seen as pi/2 (due to the context). But if "OB" is zero, "OB/OA" still makes sense, but its angle is undefined, it's the angle from OA to OB in their plane, but we can't even define a single plane in this case, as happens when OA and OB are in the same line. In such case, coplanarity can't be evaluated between a quaternion and "OB/OA" since the quaternion doesn't know the direction of the 3D vectors OB and OA, and that's not just for the zero quaternion case but for all cases where the imaginary part is zero.

I've made the suggested changes and for the confusion in and, or in the coplanar I've added brackets to make it more clear.

@mayankray2020
Copy link
Contributor Author

@oscarbenjamin @danilobellini @oscargus please review this.

@mayankray2020
Copy link
Contributor Author

@oscarbenjamin please review.

@oscarbenjamin
Copy link
Collaborator

All of the docstrings here need more explanation. Each concept should be defined. The definitions can refer to the other concepts provided they also reference them in a "see also" section. The "Returns" and "parameters" sections should be more consistent. They should specify the type of what is returned both as a Python type and also as a mathematical type like Expr: real number representing the scalar part of the quaternion.

@oscarbenjamin
Copy link
Collaborator

I don't know if you've already seen the style guide but it's here and gives examples of these things:
https://docs.sympy.org/latest/documentation-style-guide.html

@mayankray2020
Copy link
Contributor Author

All of the docstrings here need more explanation. Each concept should be defined. The definitions can refer to the other concepts provided they also reference them in a "see also" section. The "Returns" and "parameters" sections should be more consistent. They should specify the type of what is returned both as a Python type and also as a mathematical type like Expr: real number representing the scalar part of the quaternion.

I've made the suggested changes and made the doc strings bit more inofrmative. As mentioned here #20769 (comment) coplanar and ternary_coplanar have different meanings so I think it would be better to rename the method but should I allow them to take more inputs because if an user wants to check coplanarity for multiple quaternions, they can do it with 2 input coplanar method as well. What are your views on this @oscarbenjamin .

@oscarbenjamin
Copy link
Collaborator

In general in sympy methods like is_pure usually use three-valued logic. The reason is that in the symbolic case where we have something like x + y*i we do not know if x or y is zero. The attribute is_zero is used for this (rather than == 0) and gives True, False or None with None meaning "unknown". The functions fuzzy_and etc are used to handle boolean arithmetic in three-valued logic. I wonder if all of the boolean methods here should be using three-valued logic.

@mayankray2020
Copy link
Contributor Author

@oscarbenjamin please review.

@mayankray2020
Copy link
Contributor Author

@oscarbenjamin please review.

@mayankray2020
Copy link
Contributor Author

@oscarbenjamin please review.

@oscarbenjamin
Copy link
Collaborator

@danilobellini does this look good to you?

@sidhu1012
Copy link
Contributor

This needs better release notes.


assert Quaternion.ternary_coplanar(Quaternion(0, 8, 12, 16), Quaternion(0, 4, 6, 8), Quaternion(0, 2, 3, 4)) == True
assert Quaternion.ternary_coplanar(Quaternion(0, 8, 2, 6), Quaternion(0, 1, 6, 6), Quaternion(0, 0, 3, 4)) == False
raises(ValueError, lambda: Quaternion.ternary_coplanar(q0, Quaternion(0, 4, 6, 8), q1))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A test for None case can be added here.

Copy link

@danilobellini danilobellini left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the late review. Perhaps this should be rebased to at least to enforce a clean history with meaningful commit messages, unless another author is going to continue this. What do you think @oscarbenjamin?

This PR still doesn't finish every task proposed in the original issue, but it already solves most of them (11 out of 14), apart from some minor fixes I'm describing in this review. I'll comment the other 3 in another message.

Several times the leading single-sentence summary, the "Explanation" and the "Returns" sections have the same information, and sometimes with several repeated expressions within a single section. I believe there's no need for the three everywhere unless there's something new in each section. Probably all explanation sections (regarding this specific issue/PR) should have the mathematical notations (both the classical/Hamilton and the modern one, if any).

Methods scalar and vector were renamed to scalar_part and vector_part, that's okay. But, in Hamilton's notation, these would be S(q) and V(q) (using bold capital letters for S and V, something like \mathbf{S}(q) in LaTeX), and this information should be both in the summary table and in the method docstring. Something like "Given $q = a + bi + cj + dk$, returns $\mathbf{V}(q) = bi + cj + dk$." would probably suffice as an explanation in the docstrings, with a short summary like "Extract the 3D vector part of the quaternion.". The same can be said/applied to all other operators, e.g. the axis method would be explained using the the Ax(q) (or, in LaTeX, $\mathbf{Ax}(q)$) notation.

What happens on Quaternion(0, 0, 0, 0).axis()? It's returning Quaternion(nan, nan, nan, nan), but the scalar part must be zero, not NaN (and this should be a test!).

And in the "axis" explanation can include a statement like "the axis is always an imaginary unit whose square is -1", and this could be a test for all quaternions (whose vector part isn't zero), this can be tested for a literal Quaternion.

Method coplanar should be renamed to arc_coplanar, it should not be a classmethod, and its explanation is misleading, since its "3D vector" (axis) is normal to the quaternion plane, and the origin is always the same (zero). Hamilton's interpretation (and a latter definition) of a quaternion is to see them as the result of a division of two 3D vectors, so that the multiplication of this quaternion by its denominator vector would "restore" the numerator, i.e., it would "rotate" and "scale" the vector in the denominator to make it match the numerator; the plane of this quaternion is the plane containing both the numerator and denominator, and the "rotation axis" (quaternion axis) is orthogonal to both. In a comment I've explained this.

The tests for arc_coplanar are biased: they're just checking if one input is twice the other. There are several cases that it must return "True": the quaternion is always coplanar with itself (as long as it has a vector part), with its vector part scaled, with its vector part with its sign reversed, and the scalar part simply doesn't matter (it would be better if the tests didn't scale it together, actually a test should include a "w" as the scalar part, and it should still work without returning None).

Method ternary_coplanar should be renamed to vector_coplanar and its explanation must be fixed (it's explaining the other coplanar method). When renaming, the names appearing in some "See also" section should also be updated. As @praneethratna commented, this method can have more tests (it's missing a test for the None output case).


"""
if (q1.is_zero_quaternion()) or (q2.is_zero_quaternion()):
raise ValueError('Any of the provided quaternions must not be 0')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Any" sounds like one of them could be zero. "Neither" instead of "Any" (removing the "not" afterwards) would be more clear.

checks if the 3D vectors written as quaternions are in the same plane which means
the 3D vectors are parallel to the same plane. In order to return a True,
the quaternions and the origin should be in the same plane, and the real
part must be zero for it to make sense.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This explanation is misleading. The "3D vector" of a quaternion (axis) is normal to its plane, and the origin is always the same (zero).

return atan2(self.vector_part().norm(), self.scalar_part())

@classmethod
def coplanar(cls, q1, q2):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be renamed to arc_coplanar.


>>> from sympy.algebras.quaternion import Quaternion
>>> q1 = Quaternion(1, 4, 4, 4)
>>> q2 = Quaternion(2, 8, 8, 8)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to use something else instead of "2" (the only "True" in test_quaternion.py is also checking one quaternion that is twice the other), and there's no need to store these in a variable.


return atan2(self.vector_part().norm(), self.scalar_part())

@classmethod

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does arc_coplanar really need to be a classmethod?

return fuzzy_or([(aq - ar).is_zero_quaternion(), (aq + ar).is_zero_quaternion()])

@classmethod
def ternary_coplanar(cls, q1, q2, q3):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be better named as vector_coplanar, following a comment written later in the original issue.

Explanation
===========

Two general quaternions are coplanar (in this arc sense) when their axes are parallel.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a short summary of the binary coplanar operator (arc_coplanar, the other method), not an explanation of this method.


def mensor(self):
"""
Returns the log of the norm of the quaternion.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be more explicit by using "natural logarithm" instead of "log" and by adding a parenthesis like "norm (magnitude)", because in this context "norm" can have another meaning.

@danilobellini
Copy link

About the item "Add Quaternion.versor() as a new name to Quaternion.normalize()", that can be done by a simple versor = normalize, but the most important part of that item is a documentation update, whose "explanation" section should describe it using the modern "hat" notation (\hat{q} in LaTeX), the equation q/|q|, and the classical Hamilton notation \mathbf{U}(q).

With "Add Hamilton operators to the Quaternion documentation" I was thinking on a new table (perhaps in the module/class docstring or elsewhere just for the rendering it all in the "Algebras" docs) summarizing Hamilton's notation and the modern notation where it makes sense. And, to avoid confusion with the names, the item "add the deprecated terminology 'norm' and 'tensor' to the documentation" was proposed, that could be part of that table or a single paragraph before/after it.

These items weren't tackled yet, but perhaps these 3 items (as well as some documentation-related parts of my last review) can be written as another PR (after this one is fixed/finished and merged).

@praneethratna
Copy link
Contributor

praneethratna commented Dec 18, 2021

Can i take up this PR and make the changes as mentioned above and open another PR for this ? since this PR seems to be stalled

@oscarbenjamin
Copy link
Collaborator

Yes, but please preserve the commits (including the original author details) if you do that. Since this has merge conflicts I would do that by rebasing or cherrypicking the commits onto master.

@praneethratna
Copy link
Contributor

praneethratna commented Feb 2, 2022

This can be closed now as #22713 is merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add the Hamilton's "operators" to the Quaternion class (classical Quaternion approach)
7 participants