Add methods to magicl:=-lisp so that magicl:= will compare scalars.#154
Add methods to magicl:=-lisp so that magicl:= will compare scalars.#154svspire wants to merge 1 commit intoquil-lang:masterfrom
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 |
stylewarning
left a comment
There was a problem hiding this comment.
(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.
|
|
||
| (in-package #:magicl-tests) | ||
|
|
||
| (defmacro swapping-arguments-is ((predicate arg1 arg2)) |
There was a problem hiding this comment.
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.
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.
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.
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.
See comments elsewhere. I think these might be able to be eliminated.
| (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.
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.
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.
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.