Skip to content
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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/high-level/types/complex-single-float.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member

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.

(declare (ignore epsilon))
(and (=-lisp (realpart scalar1) (realpart scalar2))
(=-lisp (imagpart scalar1) (imagpart scalar2))))

(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))))
Comment on lines +37 to +61
Copy link
Member

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.


(defmethod =-lisp ((tensor1 tensor/complex-single-float) (tensor2 tensor/complex-single-float) &optional (epsilon *float-comparison-threshold*))
(unless (equal (shape tensor1) (shape tensor2))
(return-from =-lisp nil))
Expand Down
9 changes: 9 additions & 0 deletions src/high-level/types/double-float.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@
matrix/double-float
vector/double-float)

(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))

Comment on lines +25 to +33
Copy link
Member

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.

(defmethod =-lisp ((tensor1 tensor/double-float) (tensor2 tensor/double-float) &optional (epsilon *double-comparison-threshold*))
(unless (equal (shape tensor1) (shape tensor2))
(return-from =-lisp nil))
Expand Down
31 changes: 31 additions & 0 deletions src/high-level/types/single-float.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,37 @@
matrix/single-float
vector/single-float)

;; 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))

Comment on lines +25 to +38
Copy link
Member

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))

Comment on lines +39 to +55
Copy link
Member

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 ((tensor1 tensor/single-float) (tensor2 tensor/single-float) &optional (epsilon *float-comparison-threshold*))
(unless (equal (shape tensor1) (shape tensor2))
(return-from =-lisp nil))
Expand Down
75 changes: 75 additions & 0 deletions tests/abstract-tensor-tests.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,81 @@

(in-package #:magicl-tests)

(defmacro swapping-arguments-is ((predicate arg1 arg2))
Copy link
Member

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.

"Try both argument orders for a commutative predicate."
(let ((arg1sym (gensym))
(arg2sym (gensym)))
`(let ((,arg1sym ,arg1)
(,arg2sym ,arg2))
(is (,predicate ,arg1sym ,arg2sym))
(is (,predicate ,arg2sym ,arg1sym)))))

(defmacro swapping-arguments-not ((predicate arg1 arg2))
Copy link
Member

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

"Try both argument orders for a negated commutative predicate."
(let ((arg1sym (gensym))
(arg2sym (gensym)))
`(let ((,arg1sym ,arg1)
(,arg2sym ,arg2))
(is (not (,predicate ,arg1sym ,arg2sym)))
(is (not (,predicate ,arg2sym ,arg1sym))))))

(deftest test-scalar-equality ()
Copy link
Member

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.

"Test the various scalar equality predicates."
(let ((exactvalues '((-1 0 1) ; integers
(-3/2 0 3/2) ; ratios
(-1.0s0 0.0s0 1.0s0) ; single-floats
(-1.0d0 0.0d0 1.0d0) ; double-floats
(#c(-1.0s0 1.0s0) #c(-1.0s0 0.0s0) #c(1.0s0 -1.0s0) #c(1.0s0 1.0s0)) ; complex-singles
(#c(-1.0d0 1.0d0) #c(-1.0d0 0.0d0) #c(1.0d0 -1.0d0) #c(1.0d0 1.0d0)) ; complex-doubles
))
(inexactvalues '(-1.0s0 0.0s0 1.0s0 ; single-floats
-1.0d0 0.0d0 1.0d0 ; double-floats
#c(-1.0s0 1.0s0) #c(-1.0s0 0.0s0) #c(1.0s0 -1.0s0) #c(1.0s0 1.0s0) ; complex-singles
#c(-1.0d0 1.0d0) #c(-1.0d0 0.0d0) #c(1.0d0 -1.0d0) #c(1.0d0 1.0d0) ; complex-doubles
))
(small-single-delta (/ magicl::*float-comparison-threshold* 2))
(small-double-delta (/ magicl::*double-comparison-threshold* 2))
(big-single-delta (* magicl::*float-comparison-threshold* 2))
(big-double-delta (* magicl::*double-comparison-threshold* 2)))

(flet ((test-exact (group1 group2)
"Verify that magicl:= matches common-lisp:= where appropriate"
(dolist (x1 group1)
(dolist (x2 group2)
(if (common-lisp:= x1 x2)
(swapping-arguments-is (magicl:= x1 x2))
(swapping-arguments-not (magicl:= x1 x2))))))

(test-inexact (x)
"Verify that magicl:= works as expected on inexact values close to epsilon"
(let ((smalldelta (etypecase x ; a delta small enough that = should still be true
(single-float small-single-delta)
(double-float small-double-delta)
((complex single-float) small-single-delta)
((complex double-float) small-double-delta)))
(bigdelta (etypecase x ; a delta big enough that = should become false
(single-float big-single-delta)
(double-float big-double-delta)
((complex single-float) big-single-delta)
((complex double-float) big-double-delta))))

(swapping-arguments-is (magicl:= x (+ x smalldelta)))
(swapping-arguments-is (magicl:= x (- x smalldelta)))
(swapping-arguments-not (magicl:= x (+ x bigdelta)))
(swapping-arguments-not (magicl:= x (- x bigdelta)))
; offset the imaginary parts. Bonus: This also causes real/complex comparisons when x is real.
(swapping-arguments-is (magicl:= x (+ x (complex 0.0 smalldelta))))
(swapping-arguments-is (magicl:= x (- x (complex 0.0 smalldelta))))
(swapping-arguments-not (magicl:= x (+ x (complex 0.0 bigdelta))))
(swapping-arguments-not (magicl:= x (- x (complex 0.0 bigdelta)))))))

(dolist (group1 exactvalues)
(dolist (group2 exactvalues)
(test-exact group1 group2)))

(dolist (x inexactvalues)
(test-inexact x)))))

(deftest test-tensor-equality ()
"Test that tensor equality is sane for tensors of dimension 1 to 8"
(loop :for dimensions :on '(8 7 6 5 4 3 2 1) :do
Expand Down
18 changes: 9 additions & 9 deletions tests/high-level-tests.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,24 @@
(let* ((x (magicl:from-list '(6 4 2 1 -2 8 1 5 7) '(3 3)
:type 'double-float))
(d (magicl:det x)))
(is (= d -306d0))))
(is (magicl:= d -306d0))))

(deftest test-p-norm ()
"Test that the p-norm of vectors returns sane values."
;; Basic 3-4-5
(is (= 5 (magicl:norm (magicl:from-list '(3 4) '(2)))))
(is (magicl:= 5 (magicl:norm (magicl:from-list '(3 4) '(2)))))
;; One element vector should return element for all
(loop :for val :in '(-3 0 10) :do
(let ((x (magicl:from-list (list val) '(1))))
(is (= (abs val) (magicl:norm x 1)))
(is (= (abs val) (magicl:norm x 2)))
(is (= (abs val) (magicl:norm x 3)))
(is (= (abs val) (magicl:norm x :infinity)))))
(is (magicl:= (abs val) (magicl:norm x 1)))
(is (magicl:= (abs val) (magicl:norm x 2)))
(is (magicl:= (abs val) (magicl:norm x 3)))
(is (magicl:= (abs val) (magicl:norm x :infinity)))))
;; Test known values
(let ((x (magicl:from-list '(1 -2 3 4 5 -6) '(6))))
(is (= 6 (magicl:norm x :infinity)))
(is (= 21 (magicl:norm x 1)))
(is (= 9.539392 (magicl:norm x 2)))))
(is (magicl:= 6 (magicl:norm x :infinity)))
(is (magicl:= 21 (magicl:norm x 1)))
(is (magicl:= 9.539392 (magicl:norm x 2)))))

(deftest test-examples ()
"Run all of the examples. Does not check for their correctness."
Expand Down