-
Notifications
You must be signed in to change notification settings - Fork 45
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
Add methods to magicl:=-lisp so that magicl:= will compare scalars. #154
base: master
Are you sure you want to change the base?
Conversation
Add #'test-scalar-equality to test those methods. Modify #'test-p-norm to use magicl:= rather than common-lisp:= because it's (potentially) comparing floats.
Some of things to note:
|
Re
we have the same sameness in our primary implementation (SBCL) and in emerging Allegro implementation. Ignoring types short- and long-float is preexisting practice, so I think it's OK to continue. |
Re
I believe the original code is not correct, and we should be using most precise (double-float) epsilon by default because of floating-point contagion, namely Consider that In the original implementation, specifying epsilon
|
Probably worth a review by @stylewarning to be sure. Here's a reference on CL precision, contagion, and coercion: https://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node122.html |
That's not the original code; it's mine. I asked the question because I wasn't sure I'd done the right thing. So if it's wrong it's on me :-) |
OK, no problem @svspire -- I just meant for someone reading this, "original" = code being reviewed, before (my) recommended changes. Thanks |
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.
(Belated) happy holidays! Sorry for the delay on the review.
I think there's room to simplify (which, if I'm right, should be great news?!). This is definitely a welcome addition to MAGICL.
@@ -4,6 +4,81 @@ | |||
|
|||
(in-package #:magicl-tests) | |||
|
|||
(defmacro swapping-arguments-is ((predicate arg1 arg2)) |
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.
This can be a function if you'll tolerate a FUNCALL. is
doesn't need to be lexically visible.
(is (,predicate ,arg1sym ,arg2sym)) | ||
(is (,predicate ,arg2sym ,arg1sym))))) | ||
|
||
(defmacro swapping-arguments-not ((predicate arg1 arg2)) |
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.
if swapping-arguments-is
is made a function, you can use complement
on predicate
;; Might want to inline this | ||
(defun %scalar= (s1 s2 epsilon) | ||
"For equality checks of inexact scalars." | ||
(declare (type number s1 s2) ; you can use this on complex but it might be slower because of the sqrt and two multiplies. Maybe. | ||
(type real epsilon)) | ||
(<= (abs (- s1 | ||
s2)) | ||
epsilon)) | ||
|
||
(defmethod =-lisp ((scalar1 rational) (scalar2 rational) &optional epsilon) | ||
(declare (ignore epsilon)) | ||
"Rationals (integers and ratios) should be compared exactly." | ||
(common-lisp:= scalar1 scalar2)) | ||
|
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.
Could we move this to a separate file like numerical-equality.lisp
? The extensible function defined in abstract-tensor
should probably be moved there too. That way we can knock out a bunch of general/generic stuff all in one go, and we don't have it hidden here in single-float.lisp
(defmethod =-lisp ((scalar1 single-float) (scalar2 single-float) &optional (epsilon *float-comparison-threshold*)) | ||
(%scalar= scalar1 scalar2 epsilon)) | ||
|
||
(defmethod =-lisp ((scalar1 rational) (scalar2 single-float) &optional (epsilon *float-comparison-threshold*)) | ||
(%scalar= scalar1 scalar2 epsilon)) | ||
|
||
(defmethod =-lisp ((scalar1 single-float) (scalar2 rational) &optional (epsilon *float-comparison-threshold*)) | ||
(%scalar= scalar1 scalar2 epsilon)) | ||
|
||
(defmethod =-lisp ((scalar1 float) (scalar2 single-float) &optional (epsilon *float-comparison-threshold*)) | ||
"This covers comparing double-float to single-float. Use least precise epsilon." | ||
(%scalar= scalar1 scalar2 epsilon)) | ||
|
||
(defmethod =-lisp ((scalar1 single-float) (scalar2 float) &optional (epsilon *float-comparison-threshold*)) | ||
"This covers comparing single-float to double-float. Use least precise epsilon." | ||
(%scalar= scalar1 scalar2 epsilon)) | ||
|
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.
If we make the separate file, could we "just" implement a single defmethod
on real
and real
to cover all these cases not covered by rational
?
(defmethod =-lisp ((scalar1 double-float) (scalar2 double-float) &optional (epsilon *double-comparison-threshold*)) | ||
(%scalar= scalar1 scalar2 epsilon)) | ||
|
||
(defmethod =-lisp ((scalar1 rational) (scalar2 double-float) &optional (epsilon *double-comparison-threshold*)) | ||
(%scalar= scalar1 scalar2 epsilon)) | ||
|
||
(defmethod =-lisp ((scalar1 double-float) (scalar2 rational) &optional (epsilon *double-comparison-threshold*)) | ||
(%scalar= scalar1 scalar2 epsilon)) | ||
|
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.
See comments elsewhere. I think these might be able to be eliminated.
@@ -29,6 +29,37 @@ | |||
(loop :for i :below (size vector1) | |||
:sum (* (tref vector1 i) (conjugate (tref vector2 i))))) | |||
|
|||
(defmethod =-lisp ((scalar1 complex) (scalar2 complex) &optional epsilon) |
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.
See comment elsewhere about moving these functions. I think this should be moved to the file I mention elsewhere.
(defmethod =-lisp ((scalar1 complex) (scalar2 real) &optional epsilon) | ||
(let ((imagpart (imagpart scalar1)) | ||
(zero nil)) | ||
(typecase imagpart ;; TODO: do we need to care about short-floats and long-floats? On which implementations does it matter? | ||
(single-float (setf epsilon (or epsilon *float-comparison-threshold*) | ||
zero 0.0s0)) | ||
(double-float (setf epsilon (or epsilon *double-comparison-threshold*) | ||
zero 0.0d0)) | ||
; If imagpart is rational it's also guaranteed nonzero per ANSI, which means we can just go ahead and exit now | ||
(t (return-from =-lisp nil))) | ||
(and (%scalar= zero imagpart epsilon) | ||
(%scalar= (realpart scalar1) scalar2 epsilon)))) | ||
|
||
(defmethod =-lisp ((scalar1 real) (scalar2 complex) &optional epsilon) | ||
(let ((imagpart (imagpart scalar2)) | ||
(zero nil)) | ||
(typecase imagpart | ||
(single-float (setf epsilon (or epsilon *float-comparison-threshold*) | ||
zero 0.0s0)) | ||
(double-float (setf epsilon (or epsilon *double-comparison-threshold*) | ||
zero 0.0d0)) | ||
; If imagpart is rational it's also guaranteed nonzero per ANSI, which means we can just go ahead and exit now | ||
(t (return-from =-lisp nil))) | ||
(and (%scalar= zero imagpart epsilon) | ||
(%scalar= (realpart scalar2) scalar1 epsilon)))) |
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.
I might be missing some nuance but I think these can be implemented as
(and (%scalar= 0 (realpart z) epsilon)
(%scalar= x (imagpart z) epsilon))
I can't imagine finding a default for epsilon and using a typecase is needed.
(is (not (,predicate ,arg1sym ,arg2sym))) | ||
(is (not (,predicate ,arg2sym ,arg1sym)))))) | ||
|
||
(deftest test-scalar-equality () |
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.
great and thorough tests! Thanks for writing them.
@stylewarning re:
I don't really understand your comment. Recalling that there already was defaulting of epsilon in preexisting code, are you objecting to that here? |
I don't understand why we aren't just recursing on |
Aha, I see the code is figuring out what to pass if a caller does not supply But as things stand, code still ends up calling But taking @stylewarning's suggestion to heart, you could recursively call |
Add #'test-scalar-equality to test those methods.
Modify #'test-p-norm to use magicl:= rather than common-lisp:= because
it's (potentially) comparing floats.