From 58cf060e5158793d53d52776d444b7357ccc4c71 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Sun, 9 Jul 2023 08:15:27 -0700 Subject: [PATCH] add lib/math.ts --- changelog.md | 2 + package.json | 8 ++-- src/lib/index.ts | 1 + src/lib/math.ts | 86 ++++++++++++++++++++++++++++++++++++++ src/lib/structure/index.ts | 23 +--------- tests/unit/math.test.ts | 79 ++++++++++++++++++++++++++++++++++ 6 files changed, 173 insertions(+), 26 deletions(-) create mode 100644 src/lib/math.ts create mode 100644 tests/unit/math.test.ts diff --git a/changelog.md b/changelog.md index a3a2f28..93a439e 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. Dates are d #### [v0.2.0](https://github.com/janosh/elementari/compare/v0.1.8...v0.2.0) +> 8 July 2023 + - Add `Lattice.svelte` [`#41`](https://github.com/janosh/elementari/pull/41) - Show cylinder between active and hovered sites [`#40`](https://github.com/janosh/elementari/pull/40) - Fix structure controls for `atom_radius`, `same_size_atoms` [`#38`](https://github.com/janosh/elementari/pull/38) diff --git a/package.json b/package.json index 974b546..bfa85fa 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "homepage": "https://janosh.github.io/elementari", "repository": "https://github.com/janosh/elementari", "license": "MIT", - "version": "0.2.0", + "version": "0.2.2", "type": "module", "svelte": "./dist/index.js", "bugs": "https://github.com/janosh/elementari/issues", @@ -36,14 +36,14 @@ "highlight.js": "^11.8.0", "svelte": "4.0.5", "svelte-multiselect": "^10.0.0", - "three": "^0.154.0" + "three": "^0.154.0", + "@threlte/core": "6.0.0-next.11", + "@threlte/extras": "5.0.0-next.16" }, "devDependencies": { "@playwright/test": "^1.35.1", "@sveltejs/adapter-static": "2.0.2", "@sveltejs/package": "^2.1.0", - "@threlte/core": "6.0.0-next.11", - "@threlte/extras": "5.0.0-next.16", "@types/d3-array": "^3.0.5", "@types/d3-color": "^3.1.0", "@types/d3-interpolate-path": "^2.0.0", diff --git a/src/lib/index.ts b/src/lib/index.ts index 8332562..0940201 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -8,6 +8,7 @@ export * from './element' export { default as element_data } from './element/data' export * from './labels' export * from './material' +export * from './math' export * from './periodic-table' export * from './plot' export * from './structure' diff --git a/src/lib/math.ts b/src/lib/math.ts new file mode 100644 index 0000000..60a9991 --- /dev/null +++ b/src/lib/math.ts @@ -0,0 +1,86 @@ +export type Vector = [number, number, number] +export type NdVector = number[] + +export function norm(vec: NdVector): number { + return Math.sqrt(vec.reduce((acc, val) => acc + val ** 2, 0)) +} + +export function scale(vec: NdVector, factor: number): NdVector { + return vec.map((val) => val * factor) +} + +export function euclidean_dist(vec1: Vector, vec2: Vector): number { + return norm(add(vec1, scale(vec2, -1))) +} + +export function add(...vecs: NdVector[]): NdVector { + // add up any number of same-length vectors + const result = vecs[0].slice() + for (const vec of vecs.slice(1)) { + for (const [idx, val] of vec.entries()) { + result[idx] += val + } + } + return result +} + +export function dot(x1: NdVector, x2: NdVector): number | number[] { + // Handle the case where both inputs are scalars + if (typeof x1 === `number` && typeof x2 === `number`) { + return x1 * x2 + } + + // Handle the case where one input is a scalar and the other is a vector + if (typeof x1 === `number` && Array.isArray(x2)) { + throw new Error(`Scalar and vector multiplication is not supported`) + } + if (Array.isArray(x1) && typeof x2 === `number`) { + throw new Error(`Vector and scalar multiplication is not supported`) + } + + // At this point, we know that both inputs are arrays + const vec1 = x1 as number[] + const vec2 = x2 as number[] + + // Handle the case where both inputs are vectors + if (!Array.isArray(vec1[0]) && !Array.isArray(vec2[0])) { + if (vec1.length !== vec2.length) { + throw new Error(`Vectors must be of same length`) + } + return vec1.reduce((sum, val, index) => sum + val * vec2[index], 0) + } + + // Handle the case where the first input is a matrix and the second is a vector + if (Array.isArray(vec1[0]) && !Array.isArray(vec2[0])) { + const mat1 = vec1 as number[][] + if (mat1[0].length !== vec2.length) { + throw new Error( + `Number of columns in matrix must be equal to number of elements in vector`, + ) + } + return mat1.map((row) => + row.reduce((sum, val, index) => sum + val * vec2[index], 0), + ) + } + + // Handle the case where both inputs are matrices + if (Array.isArray(vec1[0]) && Array.isArray(vec2[0])) { + const mat1 = vec1 as number[][] + const mat2 = vec2 as number[][] + if (mat1[0].length !== mat2.length) { + throw new Error( + `Number of columns in first matrix must be equal to number of rows in second matrix`, + ) + } + return mat1.map((row, i) => + mat2[0].map((_, j) => + row.reduce((sum, _, k) => sum + mat1[i][k] * mat2[k][j], 0), + ), + ) + } + + // Handle any other cases + throw new Error( + `Unsupported input dimensions. Inputs must be scalars, vectors, or matrices.`, + ) +} diff --git a/src/lib/structure/index.ts b/src/lib/structure/index.ts index 5f0a406..8adc437 100644 --- a/src/lib/structure/index.ts +++ b/src/lib/structure/index.ts @@ -1,5 +1,5 @@ // Utilities for dealing with pymatgen Structures -import type { ElementSymbol } from '$lib' +import type { ElementSymbol, Vector } from '$lib' import { pretty_num } from '$lib' import element_data from '$lib/element/data' @@ -16,9 +16,6 @@ export type Species = { oxidation_state: number } -export type Vector = [number, number, number] -export type NdVector = number[] - export type Site = { species: Species[] abc: Vector @@ -104,13 +101,6 @@ export function density(structure: PymatgenStructure, prec = `.2f`) { return pretty_num(dens, prec) } -export function euclidean_dist(p1: Vector, p2: Vector): number { - const dx = p1[0] - p2[0] - const dy = p1[1] - p2[1] - const dz = p1[2] - p2[2] - return Math.sqrt(dx * dx + dy * dy + dz * dz) -} - function generate_permutations(length: number): number[][] { const result: number[][] = [] for (let i = 0; i < Math.pow(2, length); i++) { @@ -179,14 +169,3 @@ export function symmetrize_structure( return symmetrized_structure } - -export function add(...vecs: NdVector[]): NdVector { - // add up any number of same-length vectors - const result = vecs[0].slice() - for (const vec of vecs.slice(1)) { - for (const [idx, val] of vec.entries()) { - result[idx] += val - } - } - return result -} diff --git a/tests/unit/math.test.ts b/tests/unit/math.test.ts new file mode 100644 index 0000000..87cdb06 --- /dev/null +++ b/tests/unit/math.test.ts @@ -0,0 +1,79 @@ +import type { NdVector, Vector } from '$lib' +import { add, dot, euclidean_dist, norm, scale } from '$lib' +import { expect, test } from 'vitest' + +test(`norm of vector`, () => { + const vec: NdVector = [3, 4] + const expected = 5 + expect(norm(vec)).toEqual(expected) +}) + +test(`scale vector`, () => { + const vec: NdVector = [1, 2, 3] + const factor = 3 + const expected: NdVector = [3, 6, 9] + expect(scale(vec, factor)).toEqual(expected) +}) + +test(`euclidean_dist between two vectors/points`, () => { + const vec1: Vector = [1, 2, 3] + const vec2: Vector = [4, 5, 6] + expect(euclidean_dist(vec1, vec2)).toEqual(Math.sqrt(27)) // sqrt((4-1)^2 + (5-2)^2 + (6-3)^2) +}) + +test.each([ + [ + [1, 2], + [3, 4], + [4, 6], + ], + [ + [1, 2, 3], + [4, 5, 6], + [5, 7, 9], + ], + [ + [1, 2, 3, 4, 5, 6], + [7, 8, 9, 10, 11, 12], + [8, 10, 12, 14, 16, 18], + ], +])(`add`, (vec1, vec2, expected) => { + expect(add(vec1, vec2)).toEqual(expected) + // test more than 2 inputs and self-consistency (of add, scale, and norm) + expect(norm(add(vec1, vec2, scale(expected, -1)))).toEqual(0) +}) + +test.each([ + [[1, 2], [3, 4], 11], + [[1, 2, 3], [4, 5, 6], 32], +])(`add`, (vec1, vec2, expected) => { + expect(dot(vec1, vec2)).toEqual(expected) +}) + +test(`dot`, () => { + const matrix: number[][] = [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ] + const vector: NdVector = [2, 3, 4] + const expected: number[] = [20, 47, 74] // [1*2 + 2*3 + 3*4, 4*2 + 5*3 + 6*4, 7*2 + 8*3 + 9*4] + expect(dot(matrix, vector)).toEqual(expected) +}) + +test(`dot`, () => { + const matrix1: number[][] = [ + [1, 2, 3], + [4, 5, 6], + ] + const matrix2: number[][] = [ + [7, 8], + [9, 10], + [11, 12], + ] + const expected: number[][] = [ + [58, 64], // [1*7 + 2*9 + 3*11, 1*8 + 2*10 + 3*12] + [139, 154], // [4*7 + 5*9 + 6*11, 4*8 + 5*10 + 6*12] + ] + expect(dot(matrix1, matrix2)).toEqual(expected) +})