diff --git a/CHANGELOG.md b/CHANGELOG.md index bed34d3ad..bc3916d4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,33 @@ ## [unreleased] +- #448: + + - new `g/infinite?` generic with implementations for all numeric types, + complex numbers, `differential` instances. Defaults to `false` for all other + types. (Also aliased into `sicmutils.env/infinite?`). + + - The infix, TeX and JavaScript renderers (`->infix`, `->TeX` and + `->JavaScript`) all properly render `##Inf` and `##-Inf`. Infix uses the + Unicode symbol ∞, while `->TeX` uses the LaTeX command `\infty`. + Javascript's `Infinity` stands in for `##Inf` in generated JS code. + + - Complex numbers now respond `true` to `g/negative?` if their imaginary + component is zero and real component is negative, false otherwise. + + - `g/+`, `g/-`, `g//` now short circuit if there is a NUMERIC zero on either + side. This was causing bugs in cases where we allow, say, a scalar to be + added to a quaternion, and auto-convert the scalar right there (so it adds + only to the real part). OR in cases, like in the matrix PR, where we convert + the scalar in addition to `*I*`. + + - This caused some problems with `sicmutils.matrix` tests that were not well + typed. + + - The default `expt` implementation is now available as a function to call + directly (`sicmutils.generic/default-expt`) without going through the + dispatch system. + - #447 contains a grab-bag of fixes and additions, many related to complex numbers: @@ -309,7 +336,8 @@ into shape. > at our [Github Discussions](https://github.com/sicmutils/sicmutils/discussions) > page!) -This release focused on improving the expressiveness and performance of the three simplification engines in SICMUtils: +This release focused on improving the expressiveness and performance of the +three simplification engines in SICMUtils: - `sicmutils.polynomial` and `sicmutils.rational-function` are now quite well fleshed out, with full polynomial and rational function APIs and many diff --git a/src/sicmutils/complex.cljc b/src/sicmutils/complex.cljc index 015aca3fc..6ee0e8f4d 100644 --- a/src/sicmutils/complex.cljc +++ b/src/sicmutils/complex.cljc @@ -261,7 +261,13 @@ re (complex re im)))) -(defmethod g/negative? [::complex] [a] false) +(defmethod g/negative? [::complex] [a] + (and (v/zero? (imaginary a)) + (g/negative? (real a)))) + +(defmethod g/infinite? [::complex] [a] + (or (g/infinite? (real a)) + (g/infinite? (imaginary a)))) #?(:cljs ;; These are all defined explicitly in Complex.js. diff --git a/src/sicmutils/differential.cljc b/src/sicmutils/differential.cljc index 62a57f6bb..54d8289a7 100644 --- a/src/sicmutils/differential.cljc +++ b/src/sicmutils/differential.cljc @@ -1251,7 +1251,10 @@ (defbinary g/div (lift-2 g/div)) (defunary g/negative? - (fn [x] (g/negative? (finite-term x)))) + (comp g/negative? finite-term)) + +(defunary g/infinite? + (comp g/infinite? finite-term)) (defunary g/abs (fn [x] diff --git a/src/sicmutils/env.cljc b/src/sicmutils/env.cljc index 44c4a1655..070415ddf 100644 --- a/src/sicmutils/env.cljc +++ b/src/sicmutils/env.cljc @@ -43,7 +43,7 @@ = core=} :exclude [+ - * / zero? compare divide numerator denominator - #?@(:cljs [= partial])]) + #?@(:cljs [= partial infinite?])]) (:require #?(:clj [potemkin :refer [import-def import-vars]]) [sicmutils.abstract.function :as af #?@(:cljs [:include-macros true])] [sicmutils.abstract.number :as an] @@ -252,7 +252,7 @@ constant [Pi](https://en.wikipedia.org/wiki/Pi)."} [sicmutils.generic * + - / divide negate - negative? + negative? infinite? invert abs sqrt diff --git a/src/sicmutils/expression/render.cljc b/src/sicmutils/expression/render.cljc index 62050bdbf..078e93960 100644 --- a/src/sicmutils/expression/render.cljc +++ b/src/sicmutils/expression/render.cljc @@ -30,7 +30,7 @@ [sicmutils.util :as u] [sicmutils.value :as v])) -(defn ^:private make-symbol-generator [p] +(defn- make-symbol-generator [p] (let [i (atom 0)] (fn [] (symbol #?(:clj @@ -75,7 +75,7 @@ (v/integral? denom)) (str num "/" denom)))) -(defn ^:private make-infix-renderer +(defn- make-infix-renderer "Base function for infix renderers. This is meant to be specialized via options for the treatment desired. Returns a rendering function. The options are: @@ -316,12 +316,12 @@ "csc" "\\csc" "_" "\\_"}))) -(defn ^:private digit->int +(defn- digit->int [^Character d] #?(:clj (Character/digit d 10) :cljs (js/parseInt d))) -(defn ^:private n->script +(defn- n->script "Given an integer, returns a string where each digit of the integer is used as the index into the replacement map scripts, which is expected to be indexable by integers in the range [0..9]." @@ -336,6 +336,15 @@ (merge non-TeX-greek sym->unicode)) +(defn- infinity->infix + "Given some infinite value, returns a string representation of ##Inf or ##-Inf + appropriate for infix rendering, else returns `nil`." + [x] + (case x + ##Inf "∞" + ##-Inf "-∞" + nil)) + (def ^{:doc "Converts an S-expression to printable infix form. Numeric exponents are written as superscripts. Partial derivatives get subscripts."} ->infix @@ -371,34 +380,46 @@ '/ render-infix-ratio} :render-primitive (fn r [v] - (let [s (str v)] - (or (infix-sym->unicode s) - (condp re-find s - superscript-pattern - :>> (fn [[_ stem superscript]] - (if-let [n (re-matches #"[0-9]+" superscript)] - (str (r stem) (n->superscript n)) - (str (r stem) "↑" (r superscript)))) - - subscript-pattern - :>> (fn [[_ stem subscript]] - (if-let [n (re-matches #"[0-9]+" subscript)] - (str (r stem) (n->subscript n)) - (str (r stem) "_" (r subscript)))) - v)))))) - -(defn ^:private brace + (or (infinity->infix v) + (let [s (str v)] + (or (infix-sym->unicode s) + (condp re-find s + superscript-pattern + :>> (fn [[_ stem superscript]] + (if-let [n (re-matches #"[0-9]+" superscript)] + (str (r stem) (n->superscript n)) + (str (r stem) "↑" (r superscript)))) + + subscript-pattern + :>> (fn [[_ stem subscript]] + (if-let [n (re-matches #"[0-9]+" subscript)] + (str (r stem) (n->subscript n)) + (str (r stem) "_" (r subscript)))) + v))))))) + +(defn- brace "Wrap the argument, as a string, in braces" [s] (str "{" s "}")) -(defn ^:private maybe-brace +(defn- maybe-brace "Wrap the argument in braces, as a string, unless it's just a single character" [s] (if (and (string? s) (= (count s) 1)) s (brace s))) +(defn- infinity->tex + "Given some infinite value, returns a (string) representation of the LaTeX + commands required to render ##Inf or ##-Inf. + + Returns `nil` for all other inputs." + [x] + (case x + ##Inf "\\infty" + ##-Inf "-\\infty" + nil)) + (def ^{:dynamic true :doc "If true, [[->TeX]] will render down tuples as vertical matrices with square braces. Defaults to false."} @@ -515,10 +536,9 @@ '>= #(s/join " \\geq " %)} :render-primitive (fn r [v] - (cond (r/ratio? v) - (str "\\frac" (brace (r/numerator v)) (brace (r/denominator v))) - - :else + (if (r/ratio? v) + (str "\\frac" (brace (r/numerator v)) (brace (r/denominator v))) + (or (infinity->tex v) (let [s (str v)] (or (TeX-map s) (condp re-find s @@ -549,7 +569,7 @@ (if *TeX-sans-serif-symbols* (str "\\mathsf" (brace s)) (brace s)) - v))))))))) + v)))))))))) (defn ->TeX "Convert the given expression to TeX format, as a string. diff --git a/src/sicmutils/generic.cljc b/src/sicmutils/generic.cljc index 6d0681824..c96c16d3e 100644 --- a/src/sicmutils/generic.cljc +++ b/src/sicmutils/generic.cljc @@ -25,7 +25,7 @@ cljdocs](https://cljdoc.org/d/sicmutils/sicmutils/CURRENT/doc/basics/generics) for a detailed discussion of how to use and extend the generic operations defined in [[sicmutils.generic]] and [[sicmutils.value]]." - (:refer-clojure :exclude [/ + - * divide]) + (:refer-clojure :exclude [/ + - * divide #?@(:cljs [infinite?])]) (:require [sicmutils.value :as v] [sicmutils.util :as u] [sicmutils.util.def :refer [defgeneric] @@ -88,8 +88,8 @@ ([] 0) ([x] x) ([x y] - (cond (v/zero? x) y - (v/zero? y) x + (cond (v/numeric-zero? x) y + (v/numeric-zero? y) x :else (add x y))) ([x y & more] (reduce + (+ x y) more))) @@ -137,8 +137,8 @@ ([] 0) ([x] (negate x)) ([x y] - (cond (v/zero? y) x - (v/zero? x) (negate y) + (cond (v/numeric-zero? y) x + (v/numeric-zero? x) (negate y) :else (sub x y))) ([x y & more] (- x (apply + y more)))) @@ -244,7 +244,7 @@ ([] 1) ([x] (invert x)) ([x y] - (if (v/one? y) + (if (and (v/number? y) (v/one? y)) x (div x y))) ([x y & more] @@ -276,7 +276,16 @@ 0) (mul (log x) (expt x y))))}) -(defmethod expt :default [s e] +(defn ^:no-doc default-expt + "Default implementation of exponentiation for integral exponents `e`. + + This implementation uses ['Exponentation by + Squaring'](https://en.wikipedia.org/wiki/Exponentiation_by_squaring), and will + work for any type that implements `g/mul`. + + The multiplication operation is looked up once and cached at the beginning of + computation." + [s e] {:pre [(v/native-integral? e)]} (let [kind (v/kind s)] (if-let [mul' (get-method mul [kind kind])] @@ -295,6 +304,8 @@ :else (invert (expt' s (negate e))))) (u/illegal (str "No g/mul implementation registered for kind " kind))))) +(defmethod expt :default [s e] (default-expt s e)) + (defgeneric square 1) (defmethod square :default [x] (expt x 2)) @@ -347,6 +358,13 @@ (defmethod negative? :default [a] (< a (v/zero-like a))) +(defgeneric infinite? 1 + "Returns true if `a` is either numerically infinite (ie, equal to `##Inf`) or + a compound number (complex or quaterion, for example) with some infinite + component.") + +(defmethod infinite? :default [a] false) + (defgeneric abs 1) (declare integer-part) @@ -579,7 +597,12 @@ (defmethod determinant [::v/scalar] [a] a) (defmethod dimension [::v/scalar] [a] 1) (defmethod dot-product [::v/scalar ::v/scalar] [l r] (mul l r)) -(defmethod inner-product [::v/scalar ::v/scalar] [l r] (mul (conjugate l) r)) + +;; Scalars include complex numbers, but for the purposes of dot/inner-products +;; these are interpreted as pairs of real numbers, where conjugate is identity. +;; So this seems to be a sane default. +(defmethod inner-product [::v/scalar ::v/scalar] [l r] + (dot-product l r)) ;; ## Solvers @@ -621,11 +644,6 @@ (defgeneric simplify 1) (defmethod simplify :default [a] a) -(defn factorial - "Returns the factorial of `n`, ie, the product of 1 to `n` (inclusive)." - [n] - (apply * (range 1 (inc n)))) - ;; This call registers a symbol for any non-multimethod we care about. These ;; will be returned instead of the actual function body when the user ;; calls `(v/freeze fn)`, for example. @@ -643,6 +661,7 @@ clojure.core/quot 'quotient clojure.core/rem 'remainder clojure.core/neg? 'negative? + #?@(:cljs [cljs.core/infinite? 'infinite?]) clojure.core/< '< clojure.core/<= '<= clojure.core/> '> diff --git a/src/sicmutils/numbers.cljc b/src/sicmutils/numbers.cljc index ce19be431..463d073a9 100644 --- a/src/sicmutils/numbers.cljc +++ b/src/sicmutils/numbers.cljc @@ -65,6 +65,12 @@ #?(:clj (long a) :cljs (Math/trunc a))) +(defmethod g/infinite? [::v/integral] [a] false) +(defmethod g/infinite? [::v/real] [a] + #?(:clj (or (= a ##Inf) + (= a ##-Inf)) + :cljs (infinite? a))) + ;; ## Complex Operations (defmethod g/real-part [::v/real] [a] a) (defmethod g/imag-part [::v/real] [a] 0) diff --git a/src/sicmutils/ratio.cljc b/src/sicmutils/ratio.cljc index b708fe4be..c08bbe057 100644 --- a/src/sicmutils/ratio.cljc +++ b/src/sicmutils/ratio.cljc @@ -232,6 +232,8 @@ (g/lcm (core-denominator a) (core-denominator b)))) + (defmethod g/infinite? [Ratio] [a] false) + (doseq [[op f] [[g/exact-divide /] [g/quotient quot] [g/remainder rem] @@ -269,6 +271,7 @@ (defmethod g/negate [Fraction] [a] (promote (.neg ^js a))) (defmethod g/negative? [Fraction] [a] (neg? (obj/get a "s"))) + (defmethod g/infinite? [Fraction] [a] false) (defmethod g/invert [Fraction] [a] (promote (.inverse ^js a))) (defmethod g/square [Fraction] [a] (promote (.mul ^js a a))) (defmethod g/cube [Fraction] [a] (promote (.pow ^js a 3))) diff --git a/test/sicmutils/complex_test.cljc b/test/sicmutils/complex_test.cljc index 9924d16c7..821d08c49 100644 --- a/test/sicmutils/complex_test.cljc +++ b/test/sicmutils/complex_test.cljc @@ -143,6 +143,37 @@ (gt/integral-tests c/complex :exclusions skip :eq near) (gt/floating-point-tests c/complex :eq near))) + (checking "g/negative?, g/infinite?" 100 [x sg/real] + (let [z (c/complex x 0)] + (is (= (g/negative? x) + (g/negative? z)) + "A complex number is negative if its imaginary component is + zero and real is negative, false otherwise.")) + + (is (not + (g/negative? + (g/make-rectangular x 1))) + "Never negative if imaginary component is nonzero.") + + (is (not + (g/infinite? + (g/make-rectangular x x))) + "infinite? is never true for non-infinite inputs.")) + + (testing "g/infinite?" + (is (every? + g/infinite? + [(c/complex ##Inf ##Inf) + (c/complex ##-Inf ##Inf) + (c/complex ##Inf ##-Inf) + (c/complex ##-Inf ##-Inf) + (c/complex ##Inf 1) + (c/complex 1 ##Inf) + (c/complex ##-Inf 1) + (c/complex 1 ##-Inf)]) + "an infinite or negative infinite value in either slot makes the + complex number `g/infinite?`")) + (testing "add" (is (= #sicm/complex "4 + 6i" (g/add #sicm/complex "1 + 2i" diff --git a/test/sicmutils/differential_test.cljc b/test/sicmutils/differential_test.cljc index daa222050..57abe8c44 100644 --- a/test/sicmutils/differential_test.cljc +++ b/test/sicmutils/differential_test.cljc @@ -169,6 +169,28 @@ (is (v/identity? (v/identity-like diff)))) (testing "equality, comparison" + (checking "g/negative?, g/infinite?" 100 [x sg/real] + (let [elem (d/bundle-element x 1 0)] + (= (g/negative? x) + (g/negative? elem) + "negative? operates on finite-part only.") + + (is (not (g/infinite? elem)) + "infinite? is always false for real finite parts."))) + + (testing "g/infinite?" + (is (not (g/infinite? (d/bundle-element 10 ##Inf 0))) + "g/infinite? only looks at the finite part right now. Not sure how + we would get into an infinite derivative with non-infinite finite + part, but marking this test here as documentation.") + + (is (every? + g/infinite? + [(d/bundle-element ##-Inf 1 0) + (d/bundle-element ##Inf 1 0)]) + "an infinite or negative infinite value in the finite part slot + makes the differential `g/infinite?`")) + (checking "=, equiv ignore tangent parts" 100 [n sg/real-without-ratio] (is (= (d/bundle-element n 1 0) n) diff --git a/test/sicmutils/generic_test.cljc b/test/sicmutils/generic_test.cljc index 5c9b6002c..9a25a98ef 100644 --- a/test/sicmutils/generic_test.cljc +++ b/test/sicmutils/generic_test.cljc @@ -56,6 +56,7 @@ (is (= "quxquxqux" (s* "qux" 3))) (is (= "cecrcicnoeoroionlelrlilnieiriiinnenrninn" (s* "colin" "erin"))) (is (= "eceoeleienrcrorlrirnicioiliiinncnonlninn" (s* "erin" "colin")))) + (testing "add" (is (= "foobar" (s+ "foo" "bar"))) (is (= "zzz" (s+ "" "zzz"))))) @@ -104,7 +105,11 @@ (testing "div comes for free from mul and invert" (is (= (->Wrap "1/l") (g/invert l))) - (is (= (->Wrap "l*1/r") (g/div l r)))))) + (is (= (->Wrap "l*1/r") (g/div l r)))) + + (testing "unimplemented predicate behavior" + (is (not (g/infinite? l)) + "instead of an error, infinite? returns false for random types.")))) (deftest generic-freeze-behavior (testing "freeze should return symbols" @@ -125,14 +130,18 @@ (deftest generic-plus (is (= 0 (g/+)) "no args returns additive identity") + (checking "g/+" 100 [x gen/any-equatable] (is (= x (g/+ x)) "single arg should return itself, for any type.") - (is (= (if (v/zero? x) 0 x) + + (is (= (if (v/numeric-zero? x) 0 x) (g/+ x 0)) "adding a 0 works for any input. The first zero element gets returned.") + (is (= x (g/+ 0 x)) "adding a leading 0 acts as identity.") - (is (= (if (v/zero? x) 0 x) + + (is (= (if (v/numeric-zero? x) 0 x) (g/+ 0 x 0.0 0 0)) "multi-arg works as long as zeros appear."))) diff --git a/test/sicmutils/matrix_test.cljc b/test/sicmutils/matrix_test.cljc index 9c003270f..0d7000281 100644 --- a/test/sicmutils/matrix_test.cljc +++ b/test/sicmutils/matrix_test.cljc @@ -424,11 +424,18 @@ (binding [m/*careful-conversion* false] (checking "(s:transpose )==(s/transpose-outer inner) with either side empty returns an empty structure" - 100 [[l inner r] (gen/let [rows (gen/choose 0 5) cols (gen/choose 0 5)] + 100 [[l inner r] (gen/let [rows (gen/choose 0 5) + cols (gen/choose 0 5)] ( rows cols))] - (is (v/zero? - (g/- (m/s:transpose l inner r) - (s/transpose-outer inner)))))) + (if (empty? r) + (testing "in this case, the right side is fully collapsed and + empty and the left side contains a single empty structure." + (do (is (v/zero? (m/s:transpose l inner r))) + (is (empty? (s/transpose-outer inner))))) + (is (v/zero? + (g/- (m/s:transpose l inner r) + (s/transpose-outer inner))) + "left side empty generates a compatible, zero entry")))) (let [A (s/up 1 2 'a (s/down 3 4) (s/up (s/down 'c 'd) 'e)) M (m/by-rows [1 2 3] diff --git a/test/sicmutils/numbers_test.cljc b/test/sicmutils/numbers_test.cljc index f47985d6d..8c74ae72a 100644 --- a/test/sicmutils/numbers_test.cljc +++ b/test/sicmutils/numbers_test.cljc @@ -110,7 +110,18 @@ (is (= x (g/trace x)))) (checking "dimension always returns 1" 100 [x sg/real] - (is (= 1 (g/dimension x))))) + (is (= 1 (g/dimension x)))) + + (checking "dot-product, inner-product" 100 + [x sg/real y sg/real] + (is (v/= (g/* x y) + (g/dot-product x y)) + "dot-product == mul for 1-d scalars.") + + (is (= (g/dot-product x y) + (g/inner-product x y)) + "dot-product == inner-product for scalars, where conjugate acts + as identity"))) (deftest integer-generics (gt/integral-tests u/int) @@ -393,8 +404,8 @@ (letfn [(nonzero [g] (gen/fmap (fn [x] - (-> (if (v/zero? x) 1 x) - (g/remainder 10000))) + (let [small (g/remainder x 10000)] + (if (v/zero? small) 1 small))) g))] (checking "gcd" 100 [x (nonzero sg/small-integral) y (nonzero sg/small-integral) diff --git a/test/sicmutils/structure_test.cljc b/test/sicmutils/structure_test.cljc index 19089c5f9..c270195a3 100644 --- a/test/sicmutils/structure_test.cljc +++ b/test/sicmutils/structure_test.cljc @@ -701,26 +701,28 @@ (gen/tuple ))))) (deftest combining-tests - (checking " == for collapsing structures" 100 + (checking "^t == for collapsing structures" 100 [[l inner r] (gen/let [rows (gen/choose 1 5) cols (gen/choose 1 5)] ( rows cols))] (is (v/zero? (g/simplify - (g/- (g/* l (g/* inner r)) + (g/- (g/transpose + (g/* l (g/* inner r))) (g/* (g/* (g/transpose r) (s/transpose inner)) (g/transpose l))))))) - (checking " == with empty r" 100 + (checking "^t == with empty r" 100 [[l inner r] (gen/let [n (gen/choose 1 5)] ( n 0))] - (is (= (v/zero-like l) - (g/- (g/* l (g/* inner r)) - (g/* (g/transpose r) - (g/* (s/transpose inner) - (g/transpose l))))) + (is (v/zero? + (g/- (g/transpose + (g/* l (g/* inner r))) + (g/* (g/transpose r) + (g/* (s/transpose inner) + (g/transpose l))))) "unlike the previous law, this produces an uncollapsed, fully-zero structure.")) @@ -738,14 +740,17 @@ (g/- (g/* l (g/* inner r)) (g/* (g/* (s/transpose-outer inner) l) r)))))) - (checking "cols=0 transpose-outer law produces (zero-like l)" 100 + (checking "cols=0 transpose-outer law produces incompatible sides" 100 [[rows [l inner r]] (gen/let [rows (gen/choose 1 5)] (gen/tuple (gen/return rows) ( rows 0)))] - (is (= (v/zero-like l) - (g/- (g/* l (g/* inner r)) - (g/* (g/* (s/transpose-outer inner) l) r))) - "`r` has no structure to collapse the first result.")) + (is (v/zero? + (g/* l (g/* inner r))) + "the left side is a structure of zeros") + + (is (empty? + (g/* (g/* (s/transpose-outer inner) l) r)) + "the right side is an empty structure")) (testing "transpose-outer unit" (let [foo (s/down (s/down (s/up 'x 'y) @@ -917,7 +922,7 @@ "conjugate the left arg!"))) (deftest structure-generics - (testing "g/* returns a proper zero" + (testing "generic arithmetic handles zero" (is (= (s/up 0 0 0) (g/* 0 0 0 (s/up 0 0 0) 0 0)) "make sure that leading zeros don't stop the reduction and `g/*`