Skip to content

Commit ded0c1c

Browse files
committed
feat: added a tensorflow based vector
1 parent 4db2164 commit ded0c1c

File tree

4 files changed

+285
-18
lines changed

4 files changed

+285
-18
lines changed

Diff for: src/Vector.test.ts

+68-5
Original file line numberDiff line numberDiff line change
@@ -91,18 +91,81 @@ describe('Vector',()=>{
9191
expect(v_4.length()).toBeCloseTo(3.605);
9292
});
9393
});
94-
describe('haveSameDirection',()=>{
94+
describe('haveSameDirectionWith',()=>{
9595
it('should return true if the vectors have the same direction',()=>{
9696
const v_1 = new Vector([1,2,3]);
9797
const v_2 = new Vector([2,4,6]);
9898
const v_3 = new Vector([1,2,3]);
9999
const v_4 = new Vector([-1,-2,-3]);
100-
expect(v_1.haveSameDirection(v_2)).toBe(true);
101-
expect(v_1.haveSameDirection(v_3)).toBe(true);
102-
expect(v_1.haveSameDirection(v_4)).toBe(false);
100+
expect(v_1.haveSameDirectionWith(v_2)).toBe(true);
101+
expect(v_1.haveSameDirectionWith(v_3)).toBe(true);
102+
expect(v_1.haveSameDirectionWith(v_4)).toBe(false);
103103
const a = new Vector([2,4]);
104104
const b = new Vector([4,8]);
105-
expect(a.haveSameDirection(b)).toBe(true);
105+
expect(a.haveSameDirectionWith(b)).toBe(true);
106+
});
107+
});
108+
describe('haveOppositeDirectionTo',()=>{
109+
it('should return true if the vectors have the opposite direction',()=>{
110+
const v_1 = new Vector([1,2,3]);
111+
const v_2 = new Vector([2,4,6]);
112+
const v_3 = new Vector([1,2,3]);
113+
const v_4 = new Vector([-1,-2,-3]);
114+
expect(v_1.haveOppositeDirectionTo(v_2)).toBe(false);
115+
expect(v_1.haveOppositeDirectionTo(v_3)).toBe(false);
116+
expect(v_1.haveOppositeDirectionTo(v_4)).toBe(true);
117+
expect(v_1.haveOppositeDirectionTo(v_4)).toBe(true);
118+
expect(new Vector([2,4]).haveOppositeDirectionTo(new Vector([4,8]))).toBe(false);
119+
expect(new Vector([2,4]).haveOppositeDirectionTo(new Vector([-4,-8]))).toBe(true);
120+
});
121+
});
122+
describe('isPerpendicularTo',()=>{
123+
it('should return true if the vectors are perpendicular',()=>{
124+
expect(new Vector([2,4]).isPerpendicularTo(new Vector([4,8]))).toBe(false);
125+
expect(new Vector([2,2]).isPerpendicularTo(new Vector([-2,2]))).toBe(true);
126+
});
127+
});
128+
describe('crossProduct',()=>{
129+
it('should return the cross product of two vectors',()=>{
130+
const a = new Vector([2,1,1]);
131+
const b = new Vector([1,2,2]);
132+
expect(a.crossProduct(b).get()).toEqual([0,-3,3]);
133+
expect(b.crossProduct(a).get()).toEqual([0,3,-3]);
134+
});
135+
});
136+
describe('angleBetween',()=>{
137+
it('should return the angle between two vectors',()=>{
138+
const a = new Vector([0,4]);
139+
const b = new Vector([4,4]);
140+
expect(a.angleBetween(b)).toBeCloseTo(45);
141+
});
142+
});
143+
describe('negate',()=>{
144+
it('should return the negated vector',()=>{
145+
const v = new Vector([1,2,3]);
146+
expect(v.negate().get()).toEqual([-1,-2,-3]);
147+
});
148+
});
149+
describe('projectOn',()=>{
150+
it('should return the projection of the vector on another vector',()=>{
151+
const a = new Vector([8,4]);
152+
const b = new Vector([4,7]);
153+
expect(b.projectOn(a).get()).toEqual([6,3]);
154+
});
155+
});
156+
describe('withLength',()=>{
157+
it('should return the vector with the given length',()=>{
158+
const v = new Vector([2,3]);
159+
expect(v.withLength(10).length()).toEqual(10);
160+
});
161+
});
162+
describe('equalTo',()=>{
163+
it('should return true if the vectors are equal',()=>{
164+
const v_1 = new Vector([1,2,3]);
165+
const v_2 = new Vector([1,2,3]);
166+
const v_3 = new Vector([1,2,4]);
167+
expect(v_1.equalTo(v_2)).toBe(true);
168+
expect(v_1.equalTo(v_3)).toBe(false);
106169
});
107170
});
108171
describe('get',()=>{

Diff for: src/Vector.ts

+87-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { getBackend, setBackend, } from "./tensorflow_singleton";
22
import * as tf from '@tensorflow/tfjs-node';
3-
import {EPSILON,areEqual} from './util';
3+
import {EPSILON, areEqual, toDegrees} from './util';
44

55
export class Vector{
66
constructor(components:number[]){
@@ -60,10 +60,95 @@ export class Vector{
6060
* @param vector
6161
* @returns true if the current vector and the vector passed in as an argument have the same direction
6262
*/
63-
haveSameDirection(vector:Vector):boolean{
63+
haveSameDirectionWith(vector:Vector):boolean{
6464
const dotProduct = this.normalize().dotProduct(vector.normalize());
6565
return areEqual(dotProduct,1);
6666
}
67+
/**
68+
* @description returns true if the current vector and the vector passed in as an argument have opposite directions
69+
* @param vector
70+
* @returns true if the current vector and the vector passed in as an argument have opposite directions
71+
*/
72+
haveOppositeDirectionTo(vector:Vector):boolean{
73+
const dotProduct = this.normalize().dotProduct(vector.normalize());
74+
return areEqual(dotProduct,-1);
75+
}
76+
/**
77+
* @description returns true if the current vector and the vector passed in as an argument are perpendicular
78+
* @param vector
79+
* @returns true if the current vector and the vector passed in as an argument are perpendicular
80+
*/
81+
isPerpendicularTo(vector:Vector):boolean{
82+
const dotProduct = this.normalize().dotProduct(vector.normalize());
83+
return areEqual(dotProduct,0);
84+
}
85+
/**
86+
* @description returns a new vector that is the cross product of the current vector and the vector passed in as an argument
87+
* @param vector
88+
* @returns a new vector that is the cross product of the current vector and the vector passed in as an argument
89+
*/
90+
crossProduct(vector:Vector):Vector{
91+
const thisComponents = this.components.dataSync();
92+
const components = vector.components.dataSync();
93+
94+
return new Vector([
95+
thisComponents[1] * components[2] - thisComponents[2] * components[1],
96+
thisComponents[2] * components[0] - thisComponents[0] * components[2],
97+
thisComponents[0] * components[1] - thisComponents[1] * components[0]
98+
]);
99+
}
100+
/**
101+
* @description returns the angle between the current vector and the vector passed in as an argument
102+
* @param vector
103+
* @returns the angle between the current vector and the vector passed in as an argument
104+
*/
105+
angleBetween(vector:Vector):number{
106+
return toDegrees(
107+
Math.acos(
108+
this.dotProduct(vector) / (this.length() * vector.length())
109+
)
110+
);
111+
}
112+
/**
113+
* @description returns a new vector that is the current vector but with the opposite direction
114+
* @returns a new vector that is the current vector but with the opposite direction
115+
*/
116+
negate():Vector{
117+
return this.scaleBy(-1);
118+
}
119+
/**
120+
* @description returns a new vector that is the current vector projected onto the vector passed in as an argument
121+
* @param vector
122+
* @returns a new vector that is the current vector projected onto the vector passed in as an argument
123+
* @example
124+
const vector = new Vector([1,2,3])
125+
const vector2 = new Vector([1,1,1])
126+
vector.projectOn(vector2) // returns a new vector with the same direction as vector2 but with a length of 2
127+
vector.projectOn(vector2).length() // returns 2
128+
vector.projectOn(vector2).haveSameDirectionWith(vector2) // returns true
129+
vector.projectOn(vector2).isPerpendicularTo(vector2) // returns true
130+
*/
131+
projectOn(vector:Vector):Vector{
132+
const normalized = vector.normalize()
133+
const dotProductNormalized = this.dotProduct(normalized)
134+
return normalized.scaleBy(dotProductNormalized)
135+
}
136+
/**
137+
* @description returns a new vector that is the current vector but with a length of the number passed in as an argument
138+
* @param length
139+
* @returns a new vector that is the current vector but with a length of the number passed in as an argument
140+
*/
141+
withLength(length:number):Vector{
142+
return this.normalize().scaleBy(length);
143+
}
144+
/**
145+
* @description returns true if the current vector and the vector passed in as an argument are equal
146+
* @param vector
147+
* @returns true if the current vector and the vector passed in as an argument are equal
148+
*/
149+
equalTo(vector:Vector):boolean{
150+
return tf.equal(this.components,vector.components).all().arraySync()===1;
151+
}
67152
/**
68153
* @description returns the components of the vector as an array
69154
* @returns the components of the vector as an array

Diff for: src/util.test.ts

+127-10
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,132 @@ describe('util', function () {
2424
// [ -0, -0, 1, -1 ]
2525
// ]);
2626
// });
27-
describe('areEqual', () => {
28-
it('should return true if two matrices are equal', () => {
29-
expect(util.areEqual(1, 1)).toBe(true);
30-
expect(util.areEqual(2, 2)).toBe(true);
31-
});
32-
it('should return false if two matrices are not equal', () => {
33-
expect(util.areEqual(1, 3.000000000000004)).toBe(false);
34-
expect(util.areEqual(2, 1)).toBe(false);
35-
});
27+
28+
describe('areEqual', () => {
29+
it('should return true if two matrices are equal', () => {
30+
expect(util.areEqual(1, 1)).toBe(true);
31+
expect(util.areEqual(2, 2)).toBe(true);
32+
});
33+
it('should return false if two matrices are not equal', () => {
34+
expect(util.areEqual(1, 3.000000000000004)).toBe(false);
35+
expect(util.areEqual(2, 1)).toBe(false);
36+
});
37+
});
38+
describe('toDegrees', () => {
39+
it('should return the degree equivalent of the input radian', () => {
40+
expect(util.toDegrees(0)).toBe(0);
41+
expect(util.toDegrees(Math.PI)).toBe(180);
42+
expect(util.toDegrees(Math.PI / 2)).toBe(90);
3643
});
3744
});
38-
// });
45+
describe('toRadians', () => {
46+
it('should return the radian equivalent of the input degree', () => {
47+
expect(util.toRadians(0)).toBe(0);
48+
expect(util.toRadians(180)).toBe(Math.PI);
49+
expect(util.toRadians(90)).toBe(Math.PI / 2);
50+
});
51+
});
52+
});
53+
54+
/*
55+
const EPSILON = 0.00000001
56+
57+
const areEqual = (one, other, epsilon = EPSILON) =>
58+
Math.abs(one - other) < epsilon
59+
60+
const toDegrees = radians => (radians * 180) / Math.PI
61+
const toRadians = degrees => (degrees * Math.PI) / 180
62+
const sum = arr => arr.reduce((acc, value) => acc + value, 0)
63+
const withoutElementAtIndex = (arr, index) => [ ...arr.slice(0, index), ...arr.slice(index + 1) ]
64+
65+
class Vector {
66+
constructor(...components) {
67+
this.components = components
68+
}
69+
add({ components }) {
70+
return new Vector(
71+
...components.map((component, index) => this.components[index] + component)
72+
)
73+
}
74+
subtract({ components }) {
75+
return new Vector(
76+
...components.map((component, index) => this.components[index] - component)
77+
)
78+
}
79+
scaleBy(number) {
80+
return new Vector(
81+
...this.components.map(component => component * number)
82+
)
83+
}
84+
length() {
85+
return Math.hypot(...this.components)
86+
}
87+
dotProduct({ components }) {
88+
return components.reduce((acc, component, index) => acc + component * this.components[index], 0)
89+
}
90+
normalize() {
91+
return this.scaleBy(1 / this.length())
92+
}
93+
haveSameDirectionWith(other) {
94+
const dotProduct = this.normalize().dotProduct(other.normalize())
95+
return areEqual(dotProduct, 1)
96+
}
97+
haveOppositeDirectionTo(other) {
98+
const dotProduct = this.normalize().dotProduct(other.normalize())
99+
return areEqual(dotProduct, -1)
100+
}
101+
isPerpendicularTo(other) {
102+
const dotProduct = this.normalize().dotProduct(other.normalize())
103+
return areEqual(dotProduct, 0)
104+
}
105+
// 3D vectors only
106+
crossProduct({ components }) {
107+
return new Vector(
108+
this.components[1] * components[2] - this.components[2] * components[1],
109+
this.components[2] * components[0] - this.components[0] * components[2],
110+
this.components[0] * components[1] - this.components[1] * components[0]
111+
)
112+
}
113+
angleBetween(other) {
114+
return toDegrees(
115+
Math.acos(
116+
this.dotProduct(other) /
117+
(this.length() * other.length())
118+
)
119+
)
120+
}
121+
negate() {
122+
return this.scaleBy(-1)
123+
}
124+
withLength(newLength) {
125+
console.log('from util test this.normalize()',this.normalize())
126+
return this.normalize().scaleBy(newLength)
127+
}
128+
projectOn(other) {
129+
const normalized = other.normalize()
130+
return normalized.scaleBy(this.dotProduct(normalized))
131+
}
132+
equalTo({ components }) {
133+
return components.every((component, index) => areEqual(component, this.components[index]))
134+
}
135+
transform(matrix) {
136+
const columns = matrix.columns()
137+
if(columns.length !== this.components.length) {
138+
throw new Error('Matrix columns length should be equal to vector components length.')
139+
}
140+
141+
const multiplied = columns
142+
.map((column, i) => column.map(c => c * this.components[i]))
143+
const newComponents = multiplied[0].map((_, i) => sum(multiplied.map(column => column[i])))
144+
return new Vector(...newComponents)
145+
}
146+
}
147+
148+
149+
const one = new Vector(2, 3)
150+
console.log(one.length())
151+
// 3.6055512754639896
152+
const modified = one.withLength(10)
153+
// 10
154+
console.log({modified},modified.length())
155+
*/

Diff for: src/util.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,6 @@
4343
// };
4444

4545
export const EPSILON = 1e-6;
46-
export const areEqual = (a:number,b:number):boolean=>Math.abs(a-b)<EPSILON;
46+
export const areEqual = (a:number,b:number):boolean=>Math.abs(a-b)<EPSILON;
47+
export const toDegrees = radians => (radians * 180) / Math.PI;
48+
export const toRadians = degrees => (degrees * Math.PI) / 180;

0 commit comments

Comments
 (0)