From ef28e84ecc4b7dc21e66c18464fec0892633d420 Mon Sep 17 00:00:00 2001 From: Chris Watson Date: Mon, 17 Jun 2019 15:00:21 -0700 Subject: [PATCH] Adde some stuff to Vector --- src/apatite.cr | 74 ++++++++++++++++++++++++++++ src/apatite/linear_algebra/vector.cr | 67 ++++++++++--------------- 2 files changed, 101 insertions(+), 40 deletions(-) diff --git a/src/apatite.cr b/src/apatite.cr index 071fd9e..8f3235e 100644 --- a/src/apatite.cr +++ b/src/apatite.cr @@ -9,8 +9,12 @@ require "./apatite/linear_algebra" # of Crystal. module Apatite extend self + include Apatite::LinearAlgebra + class_property precision = 1e-6 + class_property approx_precision = 1e-5 + ## ## ## ## ## ## ## ## ## ## ## ## ## # # Vector Creation ## ## ## ## ## ## ## ## ## ## ## ## ## @@ -110,4 +114,74 @@ module Apatite def scalar(n, value) Matrix.scalar(n, value) end + + ## ## ## ## ## ## ## ## ## ## ## ## + # # Vector Manipulation + ## ## ## ## ## ## ## ## ## ## ## ## + + # Get the scalar (dot) product of two vectors. + # + # [https://en.wikipedia.org/wiki/Scalar_product](https://en.wikipedia.org/wiki/Scalar_product + def dot(x, y) + unless x.size == y.size + raise "Cannot compute the dot product of vectors with different dimensionality" + end + + (0...x.size).reduce(0) do |acc, i| + acc + x[i] * y[i] + end + end + + # Compute the cosine similarity between two vectors. + def similarity(x, y) + dot(x, y) / ( + Math.sqrt(dot(x, x)) * + Math.sqrt(dot(y, y)) + ) + end + + # Returns the angle between this vector and another in radians. + # If the vectors are mirrored across their axes this will return `nil`. + def angle_from(a, b) + unless a.size == b.size + raise "Cannot compute the angle between vectors with different dimensionality" + end + + dot = 0_f64 + mod1 = 0_f64 + mod2 = 0_f64 + + a.zip(b).each do |x, v| + dot += x * v + mod1 += x * x + mod2 += v * v + end + + mod1 = Math.sqrt(mod1) + mod2 = Math.sqrt(mod2) + + if mod2 * mod2 == 0 + return 0.0 + end + + theta = (dot / (mod1 * mod2)).clamp(-1, 1) + Math.acos(theta) + end + + # Returns whether the vectors are parallel to each other. + def parallel_to?(a, b) + angle = angle_from(a, b) + angle <= precision + end + + # Returns whether the vectors are antiparallel to each other. + def antiparallel_to?(a, b) + angle = angle_from(a, b) + (angle - Math::PI).abs <= precision + end + + # Returns whether the vectors are perpendicular to each other. + def perpendicular_to?(a, b) + (dot(a, b)).abs <= precision + end end diff --git a/src/apatite/linear_algebra/vector.cr b/src/apatite/linear_algebra/vector.cr index e22f2ea..207b755 100644 --- a/src/apatite/linear_algebra/vector.cr +++ b/src/apatite/linear_algebra/vector.cr @@ -371,6 +371,10 @@ module Apatite::LinearAlgebra multiply(other) end + def /(other) + divide(other) + end + def clone Vector.create(@elements.clone) end @@ -435,51 +439,35 @@ module Apatite::LinearAlgebra r == 0 ? dup : map { |x| x.to_f64 / r } end + # ditto + def normalize + to_unit_vector + end + # Returns the angle between this vector and another in radians. # If the vectors are mirrored across their axes this will return `nil`. def angle_from(vector) - v = vector.is_a?(Vector) ? vector : Vector.create(vector) + Apatite.angle_from(self, vector) + end - unless size == v.size - raise "Cannot compute the angle between vectors with different dimensionality" - end - - dot = 0_f64 - mod1 = 0_f64 - mod2 = 0_f64 - - zip(vector).each do |x, v| - dot += x * v - mod1 += x * x - mod2 += v * v - end - - mod1 = Math.sqrt(mod1) - mod2 = Math.sqrt(mod2) - - if mod2 * mod2 == 0 - return 0.0 - end - - theta = (dot / (mod1 * mod2)).clamp(-1, 1) - Math.acos(theta) + # Compute the cosine similarity between this vector and another. + def similarity(other) + Apatite.similarity(self, other) end # Returns whether the vectors are parallel to each other. def parallel_to?(vector) - angle = angle_from(vector) - angle <= Apatite.precision + Apatite.parallel_to?(self, vector) end # Returns whether the vectors are antiparallel to each other. def antiparallel_to?(vector) - angle = angle_from(vector) - (angle - Math::PI).abs <= Apatite.precision + Apatite.antiparallel_to?(self, vector) end # Returns whether the vectors are perpendicular to each other. def perpendicular_to?(vector) - (dot(vector)).abs <= Apatite.precision + Apatite.perpendicular_to?(self, vector) end # When the input is a number, this returns the result of adding @@ -503,6 +491,13 @@ module Apatite::LinearAlgebra run_binary_op(value) { |a, b| a * b } end + # When the input is a number, this returns the result of dividing + # it to all vector elements. When it's a vector, the vectors + # will be element-wise divided. + def divide(value) + run_binary_op(value) { |a, b| a / b } + end + # Returns the sum of all elements in the vector. def sum reduce(0) { |acc, i| acc + i } @@ -573,16 +568,7 @@ module Apatite::LinearAlgebra # [https://en.wikipedia.org/wiki/Scalar_product](https://en.wikipedia.org/wiki/Scalar_product) def dot(other) other = other.is_a?(Vector) ? other : Vector.create(other) - unless size == other.size - raise "Cannot compute the dot product of vectors with different dimensionality" - end - - product = 0 - (0...size).each do |i| - product += self[i] * other[i] - end - - product + Apatite.dot(self, other) end # Returns the (absolute) largest element in this vector. @@ -811,8 +797,9 @@ module Apatite::LinearAlgebra end # Call a block on the value - private def run_binary_op(value, &block : (T, T) -> T) + private def run_binary_op(value, &block : (Float64, Float64) -> Float64) if value.is_a?(Number) + value = value.to_f64 return map { |v| yield(v, value) } end