├── jest.config.js ├── .gitignore ├── webpack.config.js ├── .prettierrc.json ├── src ├── solver │ ├── constraints │ │ ├── interfaces.ts │ │ ├── energy │ │ │ ├── fixedPoint.ts │ │ │ ├── radiusLength.ts │ │ │ ├── coincident.ts │ │ │ ├── verticalLine.ts │ │ │ ├── horizontalLine.ts │ │ │ ├── concentric.ts │ │ │ ├── horizontalDistance.ts │ │ │ ├── verticalDistance.ts │ │ │ ├── distance.ts │ │ │ ├── pointOnCircle.ts │ │ │ ├── pointOnLine.ts │ │ │ ├── circleCircleTangent.ts │ │ │ ├── midpoint.ts │ │ │ ├── equalLength.ts │ │ │ ├── perpendicular.ts │ │ │ ├── lineCircleTangent.ts │ │ │ ├── parallel.ts │ │ │ └── angle.ts │ │ └── coincident.ts │ ├── interfaces.ts │ ├── optim │ │ ├── nvector.ts │ │ └── lbfgs.ts │ ├── assembly.ts │ └── entities.ts ├── math │ ├── numeric.ts │ ├── point2.ts │ ├── point3.ts │ ├── matrix2.ts │ ├── vector2.ts │ ├── vector3.ts │ └── matrix3.ts └── geometry │ ├── circle2.ts │ └── line2.ts ├── .vscode └── launch.json ├── LICENSE ├── package.json ├── test └── optim.test.ts ├── .eslintrc.json ├── README.md └── tsconfig.json /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # no NPM artifacts 2 | node_modules/ 3 | *.log 4 | 5 | # no WebStorm artifacts 6 | .idea/ 7 | 8 | # No misc macOS things 9 | .DS_Store 10 | **/.DS_Store -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | context: __dirname, 7 | entry: './src/index.js', 8 | output: { 9 | path: path.join(__dirname, 'app'), 10 | filename: 'assemble2d.js' 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/prettierrc", 3 | "bracketSameLine": false, 4 | "bracketSpacing": true, 5 | "endOfLine": "lf", 6 | "printWidth": 120, 7 | "quoteProps": "as-needed", 8 | "semi": true, 9 | "singleQuote": false, 10 | "tabWidth": 2, 11 | "trailingComma": "all", 12 | "useTabs": false 13 | } -------------------------------------------------------------------------------- /src/solver/constraints/interfaces.ts: -------------------------------------------------------------------------------- 1 | export type IndexValueMap = { [key: string]: number }; 2 | 3 | export interface SolverConstraint { 4 | f(map: IndexValueMap, x: number[]): number; 5 | // Returns g. 6 | g(map: IndexValueMap, x: number[], g: number[]): number[]; 7 | getIndexes(): string[]; 8 | getValueFromIndex(index: string): number; 9 | setValueAtIndex(index: string, value: number): void; 10 | } -------------------------------------------------------------------------------- /src/solver/constraints/energy/fixedPoint.ts: -------------------------------------------------------------------------------- 1 | export const fixedPointEnergyFunc = (x0: number, y0: number, a: number, b: number): number => { 2 | let dx = x0 - a; 3 | let dy = y0 - b; 4 | return dx * dx + dy * dy; 5 | }; 6 | 7 | export const fixedPointEnergyGrad = (x0: number, y0: number, a: number, b: number, output: number[]): number[] => { 8 | output[0] = 2 * (x0 - a); 9 | output[1] = 2 * (y0 - b); 10 | return output; 11 | }; -------------------------------------------------------------------------------- /src/solver/constraints/energy/radiusLength.ts: -------------------------------------------------------------------------------- 1 | export const radiusEnergyFunc = (x0: number, y0: number, r0: number, r: number): number => { 2 | return (r0 - r) * (r0 - r); 3 | }; 4 | 5 | export const radiusEnergyGrad = (x0: number, y0: number, r0: number, r: number, output: number[]): number[] => { 6 | const dfx0 = 0; 7 | const dfy0 = 0; 8 | const dfr0 = 2 * (r0 - r); 9 | 10 | output[0] = dfx0; 11 | output[1] = dfy0; 12 | output[2] = dfr0; 13 | return output; 14 | } -------------------------------------------------------------------------------- /src/solver/constraints/energy/coincident.ts: -------------------------------------------------------------------------------- 1 | export const coincidentEnergyFunc = (x0: number, y0: number, x1: number, y1: number): number => { 2 | let dx = x1 - x0; 3 | let dy = y1 - y0; 4 | return dx * dx + dy * dy; 5 | }; 6 | 7 | export const coincidentEnergyGrad = (x0: number, y0: number, x1: number, y1: number, output: number[]): number[] => { 8 | output[0] = -2.0 * x1 + 2.0 * x0; 9 | output[1] = 2.0 * x1 - 2.0 * x0; 10 | output[2] = -2.0 * y1 + 2.0 * y0; 11 | output[3] = 2.0 * y1 - 2.0 * y0; 12 | return output; 13 | }; 14 | -------------------------------------------------------------------------------- /src/solver/constraints/energy/verticalLine.ts: -------------------------------------------------------------------------------- 1 | export const verticalLineEnergyFunc = (x0: number, y0: number, x1: number, y1: number): number => { 2 | return (x0 - x1) * (x0 - x1); 3 | }; 4 | 5 | export const verticalLineEnergyGrad = (x0: number, y0: number, x1: number, y1: number, output: number[]): number[] => { 6 | const dfx0 = 2 * (x0 - x1); 7 | const dfy0 = 0; 8 | const dfx1 = -2 * (x0 - x1); 9 | const dfy1 = 0; 10 | 11 | output[0] = dfx0; 12 | output[1] = dfy0; 13 | output[2] = dfx1; 14 | output[3] = dfy1; 15 | return output; 16 | }; -------------------------------------------------------------------------------- /src/solver/constraints/energy/horizontalLine.ts: -------------------------------------------------------------------------------- 1 | export const horizontalLineEnergyFunc = (x0: number, y0: number, x1: number, y1: number): number => { 2 | return (y0 - y1) * (y0 - y1); 3 | }; 4 | 5 | export const horizontalLineEnergyGrad = (x0: number, y0: number, x1: number, y1: number, output: number[]): number[] => { 6 | const dfx0 = 0; 7 | const dfy0 = 2 * (y0 - y1); 8 | const dfx1 = 0; 9 | const dfy1 = -2 * (y0 - y1); 10 | 11 | output[0] = dfx0; 12 | output[1] = dfy0; 13 | output[2] = dfx1; 14 | output[3] = dfy1; 15 | return output; 16 | }; -------------------------------------------------------------------------------- /src/solver/constraints/energy/concentric.ts: -------------------------------------------------------------------------------- 1 | export const concentricEnergyFunc = (x0: number, y0: number, r0: number, x1: number, y1: number, r1: number): number => { 2 | let dx = x1 - x0; 3 | let dy = y1 - y0; 4 | return dx * dx + dy * dy; 5 | }; 6 | 7 | export const concentricEnergyGrad = (x0: number, y0: number, r0: number, x1: number, y1: number, r1: number, output: number[]): number[] => { 8 | output[0] = 2 * (x1 - x0) * (-1); 9 | output[1] = 2 * (y1 - y0) * (-1); 10 | output[2] = 0.0; 11 | output[3] = 2 * (x1 - x0); 12 | output[4] = 2 * (y1 - y0); 13 | output[5] = 0.0; 14 | return output; 15 | }; -------------------------------------------------------------------------------- /src/solver/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface SolverResult { 2 | points: { 3 | id: string; 4 | x: number; 5 | y: number; 6 | }[]; 7 | } 8 | 9 | export interface SolverInfo { 10 | points: { 11 | id: string; 12 | x: number; 13 | y: number; 14 | }[]; 15 | constraints: ConstraintInfo[]; 16 | } 17 | 18 | type ConstraintInfo = | 19 | CoincidentInfo | 20 | ConcentricInfo; 21 | 22 | export interface CoincidentInfo { 23 | type: ConstraintType.COINCIDENT; 24 | p0: string; 25 | p1: string; 26 | } 27 | 28 | export interface ConcentricInfo { 29 | type: ConstraintType.CONCENTRIC; 30 | c0: string; 31 | c1: string; 32 | } -------------------------------------------------------------------------------- /src/solver/constraints/energy/horizontalDistance.ts: -------------------------------------------------------------------------------- 1 | export const horizontalDistanceEnergyFunc = (x0: number, y0: number, x1: number, y1: number, l: number): number => { 2 | const h = (x1 - x0) * (x1 - x0) - l * l; 3 | return h * h; 4 | }; 5 | 6 | export const horizontalDistanceEnergyGrad = (x0: number, y0: number, x1: number, y1: number, l: number, output: number[]): number[] => { 7 | const h = (x1 - x0) * (x1 - x0) - l * l; 8 | 9 | const dhx0 = -2 * (x1 - x0); 10 | const dhy0 = 0; 11 | const dhx1 = 2 * (x1 - x0); 12 | const dhy1 = 0; 13 | 14 | output[0] = 2 * h * dhx0; 15 | output[1] = 2 * h * dhy0; 16 | output[2] = 2 * h * dhx1; 17 | output[3] = 2 * h * dhy1; 18 | return output; 19 | }; -------------------------------------------------------------------------------- /src/solver/constraints/energy/verticalDistance.ts: -------------------------------------------------------------------------------- 1 | export const verticalDistanceEnergyFunc = (x0: number, x1: number, y0: number, y1: number, l: number): number => { 2 | const h = (y1 - y0) * (y1 - y0) - l * l; 3 | return h * h; 4 | }; 5 | 6 | export const verticalDistanceEnergyGrad = (x0: number, x1: number, y0: number, y1: number, l: number, output: number[]): number[] => { 7 | const h = (y1 - y0) * (y1 - y0) - l * l; 8 | 9 | const dhx0 = 0; 10 | const dhy0 = -2 * (y1 - y0); 11 | const dhx1 = 0; 12 | const dhy1 = 2 * (y1 - y0); 13 | 14 | output[0] = 2 * h * dhx0; 15 | output[1] = 2 * h * dhy0; 16 | output[2] = 2 * h * dhx1; 17 | output[3] = 2 * h * dhy1; 18 | return output; 19 | }; -------------------------------------------------------------------------------- /src/solver/constraints/energy/distance.ts: -------------------------------------------------------------------------------- 1 | export const distanceEnergyFunc = (x0: number, y0: number, x1: number, y1: number, d: number): number => { 2 | let u = x1 - x0; 3 | let v = y1 - y0; 4 | const h = u * u + v * v - d * d; 5 | return h * h; 6 | } 7 | 8 | export const distanceEnergyGrad = (x0: number, y0: number, x1: number, y1: number, d: number, output: number[]): number[] => { 9 | let u = x1 - x0; 10 | let v = y1 - y0; 11 | const h = u * u + v * v - d * d; 12 | 13 | const dhx0 = -2 * u; 14 | const dhy0 = -2 * v; 15 | const dhx1 = 2 * u; 16 | const dhy1 = 2 * v; 17 | 18 | output[0] = 2 * h * dhx0; 19 | output[1] = 2 * h * dhy0; 20 | output[2] = 2 * h * dhx1; 21 | output[3] = 2 * h * dhy1; 22 | return output; 23 | }; -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Jest: current file", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}/node_modules/.bin/jest", 15 | "args": [ 16 | "${fileBasenameNoExtension}", 17 | "--config", 18 | "jest.config.js" 19 | ], 20 | "console": "integratedTerminal", 21 | "outFiles": [ 22 | "${workspaceFolder}/**/*.js" 23 | ] 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /src/solver/constraints/energy/pointOnCircle.ts: -------------------------------------------------------------------------------- 1 | export const pointOnCircleEnergyFunc = (x0: number, y0: number, cx: number, cy: number, r: number): number => { 2 | const a = x0 - cx; 3 | const b = y0 - cy; 4 | const h = a * a + b * b - r * r; 5 | return h * h; 6 | }; 7 | 8 | export const pointOnCircleEnergyGrad = (x0: number, y0: number, cx: number, cy: number, r: number, output: number[]): number[] => { 9 | const a = x0 - cx; 10 | const b = y0 - cy; 11 | const h = a * a + b * b - r * r; 12 | 13 | // h = (x0 - cx)^2 + (y0 - cy)^2 - r^2 14 | // f = h**2 15 | 16 | const dfx0 = 2 * (x0 - cx); 17 | const dfy0 = 2 * (y0 - cy); 18 | const dfcx = -2 * (x0 - cx); 19 | const dfcy = -2 * (y0 - cy); 20 | const dfr = -2 * r; 21 | 22 | output[0] = 2 * h * dfx0; 23 | output[1] = 2 * h * dfy0; 24 | output[2] = 2 * h * dfcx; 25 | output[3] = 2 * h * dfcy; 26 | output[4] = 2 * h * dfr; 27 | return output; 28 | }; -------------------------------------------------------------------------------- /src/solver/constraints/energy/pointOnLine.ts: -------------------------------------------------------------------------------- 1 | export const pointOnLineEnergyFunc = (x0: number, y0: number, x1: number, y1: number, x: number, y: number): number => { 2 | // const u = (x1 - x0); 3 | // const v = (y1 - y0); 4 | // const h = v * (x0 - x) - u * (y0 - y); 5 | const h = (y1 - y0) * (x0 - x) - (x1 - x0) * (y0 - y); 6 | return h * h; 7 | }; 8 | 9 | export const pointOnLineEnergyGrad = (x0: number, y0: number, x1: number, y1: number, x: number, y: number, output: number[]): number[] => { 10 | const h = (y1 - y0) * (x0 - x) - (x1 - x0) * (y0 - y); 11 | 12 | const dhx0 = y1 - y; 13 | const dhy0 = x - x1; 14 | const dhx1 = y - y0; 15 | const dhy1 = x0 - x; 16 | const dhx = y0 - y1; 17 | const dhy = x1 - x0; 18 | 19 | output[0] = 2 * h * dhx0; 20 | output[1] = 2 * h * dhy0; 21 | output[2] = 2 * h * dhx1; 22 | output[3] = 2 * h * dhy1; 23 | output[4] = 2 * h * dhx; 24 | output[5] = 2 * h * dhy; 25 | return output; 26 | }; -------------------------------------------------------------------------------- /src/solver/constraints/energy/circleCircleTangent.ts: -------------------------------------------------------------------------------- 1 | export const circleCircleTangentEnergyFunc = (x0: number, y0: number, r0: number, x1: number, y1: number, r1: number): number => { 2 | const a = x0 - x1; 3 | const b = y0 - y1; 4 | const c = r0 + r1; 5 | const f = a * a + b * b - c * c; 6 | return f * f; 7 | }; 8 | 9 | export const circleCircleTangentEnergyGrad = (x0: number, y0: number, r0: number, x1: number, y1: number, r1: number, output: number[]): number[] => { 10 | const a = x0 - x1; 11 | const b = y0 - y1; 12 | const c = r0 + r1; 13 | const h = a * a + b * b - c * c; 14 | 15 | // h = (x0 - x1)^2 + (y0 - y1)^2 - (r0 + r1)^2 16 | // f = h**2 17 | 18 | const dhx0 = 2 * (x0 - x1); 19 | const dhy0 = 2 * (y0 - y1); 20 | const dhr0 = -2 * (r0 + r1); 21 | const dhx1 = -2 * (x0 - x1); 22 | const dhy1 = -2 * (y0 - y1); 23 | const dhr1 = -2 * (r0 + r1); 24 | 25 | output[0] = 2 * h * dhx0; 26 | output[1] = 2 * h * dhy0; 27 | output[2] = 2 * h * dhr0; 28 | output[3] = 2 * h * dhx1; 29 | output[4] = 2 * h * dhy1; 30 | output[5] = 2 * h * dhr1; 31 | return output; 32 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Tim Bright 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/solver/constraints/energy/midpoint.ts: -------------------------------------------------------------------------------- 1 | export const midpointEnergyFunc = (x0: number, y0: number, x1: number, y1: number, x: number, y: number): number => { 2 | const a = x - x0; 3 | const b = y - y0; 4 | const c = x - x1; 5 | const d = y - y1; 6 | const h = a * a + b * b - c * c - d * d; 7 | return h * h; 8 | }; 9 | 10 | export const midpointEnergyGrad = (x0: number, y0: number, x1: number, y1: number, x: number, y: number, output: number[]): number[] => { 11 | const a = x - x0; 12 | const b = y - y0; 13 | const c = x - x1; 14 | const d = y - y1; 15 | const h = a * a + b * b - c * c - d * d; 16 | 17 | // h = (x - x0)^2 + (y - y0)^2 - (x - x1)^2 - (y - y1)^2 18 | // f = h**2 19 | 20 | const dhx0 = -2 * (x - x0); 21 | const dhy0 = -2 * (y - y0); 22 | const dhx1 = -2 * (x - x1); 23 | const dhy1 = -2 * (y - y1); 24 | const dhx = 2 * (x - x0) - 2 * (x - x1); 25 | const dhy = 2 * (y - y0) - 2 * (y - y1); 26 | 27 | output[0] = 2 * h * dhx0; 28 | output[1] = 2 * h * dhy0; 29 | output[2] = 2 * h * dhx1; 30 | output[3] = 2 * h * dhy1; 31 | output[4] = 2 * h * dhx; 32 | output[5] = 2 * h * dhy; 33 | return output; 34 | }; 35 | -------------------------------------------------------------------------------- /src/solver/constraints/energy/equalLength.ts: -------------------------------------------------------------------------------- 1 | export const equalLengthEnergyFunc = (x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number) => { 2 | const u1 = x1 - x0; 3 | const v1 = y1 - y0; 4 | const u2 = x3 - x2; 5 | const v2 = y3 - y2; 6 | const h = u1 * u1 + v1 * v1 - u2 * u2 - v2 * v2; 7 | return h * h; 8 | }; 9 | 10 | export const equalLengthEnergyGrad = (x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, output: number[]): number[] => { 11 | const u1 = x1 - x0; 12 | const v1 = y1 - y0; 13 | const u2 = x3 - x2; 14 | const v2 = y3 - y2; 15 | const h = u1 * u1 + v1 * v1 - u2 * u2 - v2 * v2; 16 | 17 | const dhx0 = -2 * u1; 18 | const dhy0 = -2 * v1; 19 | const dhx1 = 2 * u1; 20 | const dhy1 = 2 * v1; 21 | const dhx2 = 2 * u2; 22 | const dhy2 = 2 * v2; 23 | const dhx3 = -2 * u2; 24 | const dhy3 = -2 * v2; 25 | 26 | output[0] = 2 * h * dhx0; 27 | output[1] = 2 * h * dhy0; 28 | output[2] = 2 * h * dhx1; 29 | output[3] = 2 * h * dhy1; 30 | output[4] = 2 * h * dhx2; 31 | output[5] = 2 * h * dhy2; 32 | output[6] = 2 * h * dhx3; 33 | output[7] = 2 * h * dhy3; 34 | return output; 35 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "assemble2d", 3 | "version": "0.0.1", 4 | "description": "A 2D geometric constraint solver.", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "tsc -p .", 8 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx", 9 | "prettier-format": "prettier --config .prettierrc 'src/**/.ts' --write", 10 | "test": "jest" 11 | }, 12 | "keywords": [ 13 | "constraints", 14 | "solver", 15 | "geometric" 16 | ], 17 | "husky": { 18 | "hooks": { 19 | "pre-commit": "npm run lint && npm run prettier-format" 20 | } 21 | }, 22 | "author": "Tim Bright", 23 | "license": "MIT", 24 | "devDependencies": { 25 | "@types/convict": "^6.1.2", 26 | "@types/jest": "^29.5.2", 27 | "@types/node": "^20.2.5", 28 | "@types/uuid": "^9.0.2", 29 | "@types/verror": "^1.10.6", 30 | "@typescript-eslint/eslint-plugin": "^5.59.9", 31 | "@typescript-eslint/parser": "^5.59.9", 32 | "eslint": "^8.42.0", 33 | "husky": "^8.0.3", 34 | "jest": "^29.5.0", 35 | "prettier": "^2.8.8", 36 | "ts-jest": "^29.1.0", 37 | "typescript": "^5.1.3" 38 | }, 39 | "dependencies": { 40 | "convict": "^6.2.4", 41 | "dotenv": "^16.1.4", 42 | "uuid": "^9.0.0", 43 | "verror": "^1.10.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/optim.test.ts: -------------------------------------------------------------------------------- 1 | import { SolverLBFGS } from '../src/solver/optim'; 2 | 3 | describe('optimization', () => { 4 | test('should be able to optimize', async () => { 5 | const a = 1; 6 | const b = 10; 7 | 8 | // Rosenbrock function 9 | const f = (values: number[]) => { 10 | const [x, y] = values; 11 | return x * Math.exp(-x * x - y * y) + (x * x + y * y) / 20.0; 12 | }; 13 | const df = (values: number[], output: number[]): number[] => { 14 | const [x, y] = values; 15 | const df1_1 = Math.exp(-(x * x + y * y)) / 10; 16 | const df1_2 = x * Math.exp(x * x + y * y) - 20 * x * x + 10; 17 | 18 | const df1 = df1_1 * df1_2; 19 | const df2 = y * (0.1 - 2 * x * Math.exp(-(x * x + y * y))); 20 | 21 | output[0] = df1; 22 | output[1] = df2; 23 | return output; 24 | }; 25 | 26 | const solver = new SolverLBFGS(2, 3); 27 | const values = [-2, 2]; 28 | const result = await solver.solve(values, f, df); 29 | console.log(result); 30 | expect(result).toBe(true); 31 | 32 | const [v0, v1] = values; 33 | const fxmin_calc = f(values); 34 | const minimum = [-0.6691, 0]; 35 | const fxmin_solve = f(minimum); 36 | expect(Math.abs(v0 - minimum[0])).toBeLessThan(1e-4); 37 | expect(Math.abs(v1 - minimum[1])).toBeLessThan(1e-4); 38 | }); 39 | }) -------------------------------------------------------------------------------- /src/solver/constraints/energy/perpendicular.ts: -------------------------------------------------------------------------------- 1 | export const perpendicularEnergyFunc = (x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number): number => { 2 | let dx10 = x1 - x0; 3 | let dy10 = y1 - y0; 4 | let dx32 = x3 - x2; 5 | let dy32 = y3 - y2; 6 | let dot = dx10 * dx32 + dy10 * dy32; 7 | return dot * dot; 8 | }; 9 | 10 | export const perpendicularEnergyGrad = (x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, output: number[]): number[] => { 11 | let dx10 = x1 - x0; 12 | let dy10 = y1 - y0; 13 | let dx32 = x3 - x2; 14 | let dy32 = y3 - y2; 15 | 16 | const dfx0 = 2 * (-dx10 * dx32 * dx32 - dx32 * dy10 * dy32); 17 | const dfy0 = 2 * (-dx10 * dx32 * dy32 - dy10 * dy32 * dy32); 18 | const dfx1 = 2 * ( dx10 * dx32 * dx32 + dx32 * dy10 * dy32); 19 | const dfy1 = 2 * ( dx10 * dx32 * dy32 + dy10 * dy32 * dy32); 20 | const dfx2 = 2 * (-dx10 * dx10 * dx32 - dx10 * dy10 * dy32); 21 | const dfy2 = 2 * (-dx10 * dx32 * dy10 - dy10 * dy10 * dy32); 22 | const dfx3 = 2 * ( dx10 * dx10 * dx32 + dx10 * dy10 * dy32); 23 | const dfy3 = 2 * ( dx10 * dx32 * dy10 + dy10 * dy10 * dy32); 24 | 25 | output[0] = dfx0; 26 | output[1] = dfy0; 27 | output[2] = dfx1; 28 | output[3] = dfy1; 29 | output[4] = dfx2; 30 | output[5] = dfy2; 31 | output[6] = dfx3; 32 | output[7] = dfy3; 33 | return output; 34 | }; -------------------------------------------------------------------------------- /src/solver/constraints/energy/lineCircleTangent.ts: -------------------------------------------------------------------------------- 1 | export const lineCircleTangentEnergyFunc = (x0: number, y0: number, x1: number, y1: number, cx: number, cy: number, r: number): number => { 2 | const a = (x1 - x0) * (y0 - cy) - (y1 - y0) * (x0 - cx); 3 | const b = (x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0); 4 | const f = (a * a) - (b * r * r); 5 | return f * f; 6 | }; 7 | 8 | export const lineCircleTangentEnergyGrad = (x0: number, y0: number, x1: number, y1: number, cx: number, cy: number, r: number, output: number[]): number[] => { 9 | const a = cx; 10 | const b = cy; 11 | const c = x1 - x0; 12 | const d = y1 - y0; 13 | 14 | const f1 = c * (y0 - b) - d * (x0 - a); 15 | const f2 = c * c + d * d; 16 | const f = (f1 * f1) - (f2 * r * r); 17 | 18 | const dfx0 = 2 * f * (-2*a*d*d + 2*b*c*d - 2*c*d*y0 + 2*d*d*x0); 19 | const dfy0 = 2 * f * ( 2*a*c*d - 2*b*c*c + 2*c*c*y0 - 2*c*d*x0); 20 | const dfx1 = 2 * f * (-2*a*b*d + 2*b*c*c + 2*a*d*y0 + 2*b*d*x0 - 4*b*c*y0 - 2*c*r*r + 2*c*y0*y0 - 2*d*x0*y0); 21 | const dfy1 = 2 * f * ( 2*a*a*d - 2*a*b*c + 2*a*c*y0 + 2*b*c*x0 - 4*a*d*x0 - 2*d*r*r - 2*c*x0*y0 + 2*d*x0*x0); 22 | const dfcx = 2 * f * ( 2*a*d*d - 2*b*c*d + 2*c*d*y0 - 2*d*d*x0); 23 | const dfcy = 2 * f * (-2*a*c*d + 2*b*c*c - 2*c*c*y0 + 2*c*d*x0); 24 | const dfr = 2 * f * (-2*c*c*r - 2*d*d*r); 25 | 26 | output[0] = dfx0; 27 | output[1] = dfy0; 28 | output[2] = dfx1; 29 | output[3] = dfy1; 30 | output[4] = dfcx; 31 | output[5] = dfcy; 32 | output[6] = dfr; 33 | return output; 34 | }; -------------------------------------------------------------------------------- /src/solver/constraints/energy/parallel.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * See Eberly, Robust and Error-free Geometric Computing (2020), page 23. 3 | */ 4 | export const parallelEnergyFunc = (x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number): number => { 5 | let dx10 = x1 - x0; 6 | let dy10 = y1 - y0; 7 | let dx32 = x3 - x2; 8 | let dy32 = y3 - y2; 9 | let f = dx10 * dx10 * dy32 * dy32 10 | - 2 * dx10 * dx32 * dy10 * dy32 11 | + dx32 * dx32 * dy10 * dy10; 12 | return f; 13 | }; 14 | 15 | export const parallelEnergyGrad = (x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, output: number[]): number[] => { 16 | let dx10 = x1 - x0; 17 | let dy10 = y1 - y0; 18 | let dx32 = x3 - x2; 19 | let dy32 = y3 - y2; 20 | 21 | const dfx0 = 2 * (-dx10 * dy32 * dy32 + dx32 * dy10 * dy32); 22 | const dfy0 = 2 * ( dx10 * dx32 * dy32 - dy10 * dx32 * dx32); 23 | const dfx1 = 2 * ( dx10 * dy32 * dy32 - dx32 * dy10 * dy32); 24 | const dfy1 = 2 * (-dx10 * dx32 * dy32 + dy10 * dx32 * dx32); 25 | const dfx2 = 2 * ( dx10 * dy10 * dy32 - dx32 * dy10 * dy10); 26 | const dfy2 = 2 * (-dy32 * dx10 * dx10 + dx10 * dx32 * dy10); 27 | const dfx3 = 2 * (-dx10 * dy10 * dy32 + dx32 * dy10 * dy10); 28 | const dfy3 = 2 * ( dy32 * dx10 * dx10 - dx10 * dx32 * dy10); 29 | 30 | output[0] = dfx0; 31 | output[1] = dfy0; 32 | output[2] = dfx1; 33 | output[3] = dfy1; 34 | output[4] = dfx2; 35 | output[5] = dfy2; 36 | output[6] = dfx3; 37 | output[7] = dfy3; 38 | return output; 39 | }; -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "semistandard" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "plugins": "@typescript-eslint", 12 | "rules": { 13 | "@typescript-eslint/no-use-before-define": "off", 14 | "arrow-parens": ["error", "parens"], 15 | "brace-style": ["error", "1tbs"], 16 | "global-require": "off", 17 | "implicit-arrow-linebreak": "off", 18 | "import/extensions": "off", 19 | "import/no-unresolved": "off", 20 | "import/order": "off", 21 | "import/prefer-default-export": "off", 22 | "max-len": ["error", { "code": 120, "tabWidth": 2 }], 23 | "no-await-in-loop": "off", 24 | "no-multi-spaces": "off", 25 | "no-param-reassign": "off", 26 | "no-plusplus": "off", 27 | "no-restricted-syntax": [ 28 | "error", 29 | { 30 | "selector": "LabeledStatement", 31 | "message": "Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand." 32 | }, 33 | { 34 | "selector": "WithStatement", 35 | "message": "`with` is disallowed in strict mode because it makes code impossible to predict and optimize." 36 | } 37 | ], 38 | "no-underscore-dangle": "off", 39 | "no-restricted-globals": "off", 40 | "no-unused-expressions": [ 41 | "error", 42 | { 43 | "allowShortCircuit": true, 44 | "allowTernary": true 45 | } 46 | ], 47 | "no-use-before-define": "off", 48 | "object-curly-newline": [ 49 | "error", 50 | { 51 | "consistent": true, 52 | "multiline": true 53 | } 54 | ], 55 | "operator-linebreak": "off", 56 | "prefer-default-export": "off", 57 | "quotes": ["error", "double"], 58 | "no-shadow": "off", 59 | "@typescript-eslint/no-shadow": "error", 60 | "no-unused-vars": "off", 61 | "@typescript-eslint/no-unused-vars": "error" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/solver/optim/nvector.ts: -------------------------------------------------------------------------------- 1 | import { VError } from "verror"; 2 | 3 | export const VectorN = { 4 | new: (size: number): number[] => { 5 | const v = []; 6 | for (let i = 0; i < size; i++) { 7 | v[i] = 0; 8 | } 9 | return v; 10 | }, 11 | size: (v: number[]): number => { 12 | return v.length; 13 | }, 14 | assertSameSize: (v: number[], w: number[]): void => { 15 | if (VectorN.size(v) !== VectorN.size(w)) { 16 | throw new VError("number[].dot(): sizes of vector are mismatched."); 17 | } 18 | return; 19 | }, 20 | // Copies the values from the argument into v and returns v. 21 | copy: (dest: number[], src: number[]): number[] => { 22 | VectorN.assertSameSize(dest, src); 23 | for (let i = 0; i < dest.length; i++) { 24 | dest[i] = src[i]; 25 | } 26 | return dest; 27 | }, 28 | // Adds the vectors in-place and returns this number[], similar to axpy. 29 | scale: (a: number, x: number[], output: number[]): number[] => { 30 | VectorN.assertSameSize(output, x); 31 | if (Number.isNaN(a) || !Number.isFinite(a)) { 32 | throw new VError("number[].scaleAndAdd(): scale is not valid"); 33 | } 34 | 35 | for (let i = 0; i < output.length; i++) { 36 | output[i] = a * x[i]; 37 | } 38 | return output; 39 | }, 40 | // Adds the vectors in-place and returns this number[], similar to axpy. 41 | scaleAndAdd: (y: number[], a: number, x: number[], output: number[]): number[] => { 42 | VectorN.assertSameSize(y, x); 43 | VectorN.assertSameSize(y, output); 44 | if (Number.isNaN(a) || !Number.isFinite(a)) { 45 | throw new VError("number[].scaleAndAdd(): scale is not valid"); 46 | } 47 | 48 | for (let i = 0; i < y.length; i++) { 49 | output[i] = y[i] + a * x[i]; 50 | } 51 | return output; 52 | }, 53 | dot: (v: number[], w: number[]): number => { 54 | VectorN.assertSameSize(v, w); 55 | let sum = 0; 56 | for (let i = 0; i < v.length; i++) { 57 | sum += w[i] * v[i]; 58 | } 59 | return sum; 60 | } 61 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Assemble2D 2 | A 2D geometric constraint solver. 3 | 4 | ## Usage 5 | 6 | ### List of Constraints: 7 | - Coincident 8 | - Concentric 9 | - Fixed 10 | - Tangent 11 | - Angle 12 | - Parallel 13 | - Perpendicular 14 | - Distance 15 | - Equal Length 16 | - Horizontal 17 | - Vertical 18 | - Length 19 | - Radius 20 | - Horizontal Distance 21 | - Vertical Distance 22 | - Midpoint 23 | 24 | ## Theory of Operation 25 | 26 | The constraints are expressed as mathematical "energy" functions of the parameters of various geometric entities. Each constraint encodes a relationship between a few of these parameters and returns an "energy" value: 27 | 28 | $$ f_(x_1, x_2, ... , x_m) = E $$ 29 | 30 | The sum of all the "local" constraint functions determines the "global" energy in a given system: 31 | 32 | $$ \mathbf{x} = [x_1, x_2, ... , x_n] $$ 33 | $$ F(\mathbf{x}) = f_1(\mathbf{x}) + f_2(\mathbf{x}) + ... + f_n(\mathbf{x}) $$ 34 | 35 | Here, $ \mathbf{x} $ is simply a vector of each independent parameter of the geometric entities (e.g. coordinate value, radius, length, distance, etc.). Each $ f_i(\mathbf{x}) $ involves only a select few of the entries in the $ \mathbf{x} $ vector. The gradient can be calculated in a similar manner: 36 | 37 | $$ \nabla F(\mathbf{x}) = g(\mathbf{x}) = \nabla f_1(\mathbf{x}) + \nabla f_2(\mathbf{x}) + ... + \nabla f_n(\mathbf{x}) $$ 38 | 39 | The key idea here is to construct the global energy function in such a way that the minimum occurs at parameter values that would satisfy the constraints specified for the system: 40 | 41 | $$ \underset{\mathbf{x}}{\text{min}} \, F(\mathbf{x}) $$ 42 | 43 | In this implementation, unconstrained optimization (L-BFGS) is used to find the minimum. The energy functions are quadratic and each of their minimums should be zero when the constraints are satisfied. Thus, the global energy of the system should theoretically be zero when the local constraints are satisfied. If the minimum does not occur at zero, then the system can be said to be overconstrained. 44 | 45 | Each parameter has a given "index" (or ID) and must be provided with the starting value. The solver then generates a hash table with the "indexes" as keys and the array offset as values when it constructs the global parameter vector (i.e. the $\mathbf{x}$ vector). For each local energy function, the hash table indexes the right values in the global parameter vector to calculate the local energy functions, then adds them all up to get the global energy function value. It does similarly for the energy gradient function values. 46 | -------------------------------------------------------------------------------- /src/solver/assembly.ts: -------------------------------------------------------------------------------- 1 | import VError from 'verror'; 2 | 3 | import { IndexValueMap, SolverConstraint } from './constraints/interfaces'; 4 | import { SolverLBFGS } from './optim/lbfgs'; 5 | 6 | export class ConstraintSystem { 7 | private _constraints: SolverConstraint[]; 8 | private _tmpX: number[]; 9 | private _tmpGrad: number[]; 10 | private _indexValueMap: IndexValueMap; 11 | 12 | constructor() { 13 | this._constraints = []; 14 | this._tmpX = []; 15 | this._tmpGrad = []; 16 | this._indexValueMap = {}; 17 | } 18 | 19 | public addConstraint(constraint: SolverConstraint): void { 20 | this._constraints.push(constraint); 21 | } 22 | 23 | public objectiveFunc = (x: number[]): number => { 24 | let result = 0; 25 | for (const constraint of this._constraints) { 26 | result += constraint.f(this._indexValueMap, x); 27 | } 28 | return result; 29 | } 30 | 31 | public objectiveGrad = (x: number[]): number[] => { 32 | // reset gradient array 33 | for (let i = 0; i < x.length; i++) { 34 | this._tmpGrad[i] = 0; 35 | } 36 | 37 | // construct gradient 38 | for (const constraint of this._constraints) { 39 | constraint.g(this._indexValueMap, x, this._tmpGrad); 40 | } 41 | return this._tmpGrad; 42 | } 43 | 44 | public async solve(): Promise { 45 | // construct index hash map and value array 46 | let globalPosition = 0; 47 | const x = this._tmpX; 48 | const indexValueMap: { [key: string]: number } = {}; 49 | 50 | // initialize optimization problem 51 | for (const constraint of this._constraints) { 52 | const indexes = constraint.getIndexes(); 53 | for (const index of indexes) { 54 | // load unique indexes into array 55 | if (indexValueMap[index] === undefined) { 56 | indexValueMap[index] = globalPosition; 57 | x[globalPosition] = constraint.getValueFromIndex(index); 58 | globalPosition++; 59 | } 60 | } 61 | } 62 | this._indexValueMap = indexValueMap; 63 | 64 | const solver = new SolverLBFGS(globalPosition, 4); 65 | const result = await solver.solve(x, this.objectiveFunc, this.objectiveGrad); 66 | if (result.success) { 67 | // load values into solver entities 68 | for (const constraint of this._constraints) { 69 | const cIndexes = constraint.getIndexes(); 70 | for (const index of cIndexes) { 71 | const arrPos = indexValueMap[index]; 72 | const value = x[arrPos]; 73 | if (value === undefined) { 74 | throw new VError('Missing value for index %s', index); 75 | } 76 | constraint.setValueAtIndex(index, value); 77 | } 78 | } 79 | } 80 | 81 | // return result 82 | return result.success; 83 | } 84 | } -------------------------------------------------------------------------------- /src/solver/constraints/energy/angle.ts: -------------------------------------------------------------------------------- 1 | export const angleEnergyFunc = (x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, theta: number): number => { 2 | const u0 = x1 - x0; 3 | const v0 = y1 - y0; 4 | const u1 = x3 - x2; 5 | const v1 = y3 - y2; 6 | 7 | const lenU = Math.sqrt(u0 * u0 + v0 * v0); 8 | const lenV = Math.sqrt(u1 * u1 + v1 * v1); 9 | 10 | const dot = (u0 * u1 + v0 * v1) / (lenU * lenV); 11 | const angle = Math.acos(dot) - theta; 12 | return angle * angle; 13 | } 14 | 15 | export const angleEnergyGrad = (x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, theta: number, output: number[]): number[] => { 16 | const ux = x1 - x0; 17 | const uy = y1 - y0; 18 | const vx = x3 - x2; 19 | const vy = y3 - y2; 20 | 21 | const w = ux * vy - uy * vx; // |u x v| 22 | const z = ux * vx + uy * vy; // u . v 23 | 24 | if (w === 0 && z === 0) { 25 | // this should only happen when some of the points are coincident 26 | // TODO: should this be allowed? maybe we should just return 0? 27 | output[0] = Number.MAX_VALUE; 28 | output[1] = Number.MAX_VALUE; 29 | output[2] = Number.MAX_VALUE; 30 | output[3] = Number.MAX_VALUE; 31 | output[4] = Number.MAX_VALUE; 32 | output[5] = Number.MAX_VALUE; 33 | output[6] = Number.MAX_VALUE; 34 | output[7] = Number.MAX_VALUE; 35 | return output; 36 | } 37 | 38 | // h = |u x v| / (u . v) = tan(v_angle) 39 | // f = [atan(h) - theta]^2 40 | // df/dx = 2 * [atan(h) - theta] * (1 / 1 + h^2) * dh/dx 41 | // = 2 * [atan(h) - theta] * (z^2 / z^2 + w^2) * dh/dx 42 | 43 | const dwx0 = y2 - y3; 44 | const dwy0 = x3 - x2; 45 | const dwx1 = y3 - y2; 46 | const dwy1 = x2 - x3; 47 | const dwx2 = y1 - y0; 48 | const dwy2 = x0 - x1; 49 | const dwx3 = y0 - y1; 50 | const dwy3 = x1 - x0; 51 | 52 | const dzx0 = x2 - x3; 53 | const dzy0 = y2 - y3; 54 | const dzx1 = x3 - x2; 55 | const dzy1 = y3 - y2; 56 | const dzx2 = x0 - x1; 57 | const dzy2 = y0 - y1; 58 | const dzx3 = x1 - x0; 59 | const dzy3 = y1 - y0; 60 | 61 | const dhx0 = (z * dwx0 - w * dzx0) / (z * z); 62 | const dhy0 = (z * dwy0 - w * dzy0) / (z * z); 63 | const dhx1 = (z * dwx1 - w * dzx1) / (z * z); 64 | const dhy1 = (z * dwy1 - w * dzy1) / (z * z); 65 | const dhx2 = (z * dwx2 - w * dzx2) / (z * z); 66 | const dhy2 = (z * dwy2 - w * dzy2) / (z * z); 67 | const dhx3 = (z * dwx3 - w * dzx3) / (z * z); 68 | const dhy3 = (z * dwy3 - w * dzy3) / (z * z); 69 | 70 | const t = 2 * (Math.atan2(z, w) - theta) * (z * z) / (z * z + w * w); 71 | 72 | output[0] = t * dhx0; 73 | output[1] = t * dhy0; 74 | output[2] = t * dhx1; 75 | output[3] = t * dhy1; 76 | output[4] = t * dhx2; 77 | output[5] = t * dhy2; 78 | output[6] = t * dhx3; 79 | output[7] = t * dhy3; 80 | return output; 81 | }; -------------------------------------------------------------------------------- /src/math/numeric.ts: -------------------------------------------------------------------------------- 1 | import VError from "verror"; 2 | 3 | /// Checks if the tolerance is positive but under unity. 4 | export const isValidTolerance = (tol: number): boolean => { 5 | return tol >= 0.0 && tol < 1.0; 6 | } 7 | 8 | export const isOverflow = (x: number): boolean => { 9 | return x === Number.POSITIVE_INFINITY || x === Number.NEGATIVE_INFINITY; 10 | } 11 | 12 | export const isNaN = (x: number): boolean => { 13 | return Number.isNaN(x); 14 | } 15 | 16 | export const resultFromCalculation = async (x: number): Promise => { 17 | if (isOverflow(x)) { 18 | throw new VError("result_from_calculation(): overflowed result"); 19 | } else if (isNaN(x)) { 20 | throw new VError("result_from_calculation(): number is NaN"); 21 | } else { 22 | return x; 23 | } 24 | } 25 | 26 | // Computes the 2-norm of a vector (i.e. sqrt(a^2 + b^2)) in a numerically-stable way. 27 | export const nrm2 = (a: number, b: number): number => { 28 | if (a === 0.0 && b === 0.0) { 29 | return 0.0; 30 | } 31 | 32 | const x = Math.abs(a); 33 | const y = Math.abs(b); 34 | const u = Math.max(x, y); 35 | const t = Math.min(x, y) / u; 36 | return u * Math.sqrt(1.0 + t * t); 37 | } 38 | 39 | // inscribedAngleTriangle returns inscribed angle between 2 sides of a triangle. 40 | export const inscribedAngleTriangle = (a: number, b: number, c: number): number => { 41 | const mu = (c > b) ? 42 | b - (a - c) : 43 | c - (a - b); 44 | 45 | const t1 = (a - b) + c; 46 | const t2 = a + (b + c); 47 | const t3 = (a - c) + b; 48 | 49 | const t = (t1 * mu) / (t2 * t3); 50 | return 2.0 * Math.atan(Math.sqrt(t)); 51 | } 52 | 53 | // Splits a 53-bit number double precision number into two 26-bit numbers. Returns [x,y] such that x + y = a. 54 | // The value x contains the more significant bits of the sum, and the value y contains the less significant bits. 55 | // See https://www.sciencedirect.com/science/article/pii/S0890540112000715 for usage. 56 | const split = (a: number): [number, number] => { 57 | const factor = 2.0e27 + 1.0; 58 | const c = factor * a; 59 | const x = c - (c - a); 60 | const y = a - x; 61 | return [x, y]; 62 | } 63 | 64 | // Computes the product of 2 numbers. Returns [x,y] such that x + y = a + b. 65 | // The value x contains the more significant bits of the sum, and the value y contains the less significant bits. 66 | // See https://www.sciencedirect.com/science/article/pii/S0890540112000715 for usage. 67 | export const twoProduct = (a: number, b: number): [number, number] => { 68 | const x = a * b; 69 | const [a1, a2] = split(a); 70 | const [b1, b2] = split(b); 71 | const y = a2 * b2 - (((x - a1 * b1) - a2 * b1) - a1 * b2); 72 | return [x, y]; 73 | } 74 | 75 | // Computes the sum of 2 numbers. Returns [x,y] such that x + y = a + b. 76 | // The value x contains the more significant bits of the sum, and the value y contains the less significant bits. 77 | // See https://www.sciencedirect.com/science/article/pii/S0890540112000715 for usage. 78 | export const twoSum = (a: number, b: number): [number, number] => { 79 | const x = a + b; 80 | const y = (Math.abs(a) >= Math.abs(b)) ? 81 | b - (x - a) : 82 | a - (x - b); 83 | return [x, y]; 84 | } -------------------------------------------------------------------------------- /src/solver/constraints/coincident.ts: -------------------------------------------------------------------------------- 1 | import VError from "verror"; 2 | 3 | import { SolverPoint } from "../entities"; 4 | import { coincidentEnergyFunc, coincidentEnergyGrad } from "./energy/coincident"; 5 | import { SolverConstraint, IndexValueMap } from "./interfaces"; 6 | 7 | export class CoincidentConstraint implements SolverConstraint { 8 | private _p0: SolverPoint; 9 | private _p1: SolverPoint; 10 | 11 | private _p0xIndex: string; 12 | private _p0yIndex: string; 13 | private _p1xIndex: string; 14 | private _p1yIndex: string; 15 | 16 | private _globalValues: number[]; 17 | private _grad: number[]; 18 | 19 | /** 20 | * Coincident constraint between two points. 21 | * @param p0 The first solver point. 22 | * @param p1 The second solver point. 23 | */ 24 | public constructor(p0: SolverPoint, p1: SolverPoint) { 25 | this._p0 = p0; 26 | this._p1 = p1; 27 | this._grad = [0, 0, 0, 0]; 28 | this._globalValues = [0, 0, 0, 0]; 29 | 30 | this._p0xIndex = p0.xIndex; 31 | this._p0yIndex = p0.yIndex; 32 | this._p1xIndex = p1.xIndex; 33 | this._p1yIndex = p1.yIndex; 34 | } 35 | 36 | private _loadGlobalValuesIntoTemp(map: IndexValueMap, x: number[]) { 37 | const ipx = map[this._p0xIndex]; 38 | const ipy = map[this._p0yIndex]; 39 | const iqx = map[this._p1xIndex]; 40 | const iqy = map[this._p1yIndex]; 41 | if (!ipx || !ipy || !iqx || !iqy) { 42 | throw new VError('Missing index for coincident constraint'); 43 | } 44 | const px = x[ipx]; 45 | const py = x[ipy]; 46 | const qx = x[iqx]; 47 | const qy = x[iqy]; 48 | if (px === undefined || py === undefined || qx === undefined || qy === undefined) { 49 | throw new VError('Missing value for coincident constraint'); 50 | } 51 | this._globalValues[0] = px; 52 | this._globalValues[1] = py; 53 | this._globalValues[2] = qx; 54 | this._globalValues[3] = qy; 55 | } 56 | 57 | public getIndexes(): string[] { 58 | return [this._p0xIndex, this._p0yIndex, this._p1xIndex, this._p1yIndex]; 59 | } 60 | 61 | public f(map: IndexValueMap, x: number[]): number { 62 | this._loadGlobalValuesIntoTemp(map, x); 63 | const [px, py, qx, qy] = this._globalValues; 64 | return coincidentEnergyFunc(px, py, qx, qy); 65 | } 66 | 67 | // Returns g. 68 | public g(map: IndexValueMap, x: number[], g: number[]): number[] { 69 | this._loadGlobalValuesIntoTemp(map, x); 70 | const [px, py, qx, qy] = this._globalValues; 71 | coincidentEnergyGrad(px, py, qx, qy, this._grad); 72 | const [gpx, gpy, gqx, gqy] = this._grad; 73 | const ipx = map[this._p0xIndex]; 74 | const ipy = map[this._p0yIndex]; 75 | const iqx = map[this._p1xIndex]; 76 | const iqy = map[this._p1yIndex]; 77 | g[ipx] += gpx; 78 | g[ipy] += gpy; 79 | g[iqx] += gqx; 80 | g[iqy] += gqy; 81 | return g; 82 | } 83 | 84 | public getValueFromIndex(index: string): number { 85 | if (index === this._p0xIndex) { 86 | return this._p0.x; 87 | } else if (index === this._p0yIndex) { 88 | return this._p0.y; 89 | } else if (index === this._p1xIndex) { 90 | return this._p1.x; 91 | } else if (index === this._p1yIndex) { 92 | return this._p1.y; 93 | } else { 94 | throw new VError(`Index ${index} not found in coincident constraint`); 95 | } 96 | } 97 | 98 | public setValueAtIndex(index: string, value: number) { 99 | if (index === this._p0xIndex) { 100 | this._p0.setX(value); 101 | } else if (index === this._p0yIndex) { 102 | this._p0.setY(value); 103 | } else if (index === this._p1xIndex) { 104 | this._p1.setX(value); 105 | } else if (index === this._p1yIndex) { 106 | this._p1.setY(value); 107 | } else { 108 | throw new VError(`Index ${index} not found in coincident constraint`); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/solver/entities.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | import { VError } from 'verror'; 3 | 4 | export interface SolverEntity { 5 | // must return entities in a deterministic way 6 | // getIndexes(): string[]; 7 | // hasIndex(index: string): boolean; 8 | // getValueFromIndex(index: string): Promise; 9 | } 10 | 11 | export class SolverScalar implements SolverEntity { 12 | private _value: number; 13 | private _index: string; 14 | 15 | constructor(value: number) { 16 | this._value = value; 17 | this._index = uuidv4(); 18 | } 19 | 20 | public get index(): string { 21 | return this._index; 22 | } 23 | 24 | public get value(): number { 25 | return this._value; 26 | } 27 | 28 | public set value(value: number) { 29 | this._value = value; 30 | } 31 | } 32 | 33 | export class SolverPoint { 34 | private _x: SolverScalar; 35 | private _y: SolverScalar; 36 | 37 | constructor(x: number, y: number) { 38 | this._x = new SolverScalar(x); 39 | this._y = new SolverScalar(y); 40 | } 41 | 42 | public set(x: number, y: number) { 43 | this._x.value = x; 44 | this._y.value = y; 45 | } 46 | 47 | public setX(x: number) { 48 | this._x.value = x; 49 | } 50 | 51 | public setY(y: number) { 52 | this._y.value = y; 53 | } 54 | 55 | public get x(): number { 56 | return this._x.value; 57 | } 58 | 59 | public get y(): number { 60 | return this._y.value; 61 | } 62 | 63 | public get xIndex(): string { 64 | return this._x.index; 65 | } 66 | 67 | public get yIndex(): string { 68 | return this._y.index; 69 | } 70 | } 71 | 72 | export class SolverLine implements SolverEntity { 73 | public _p0: SolverPoint; 74 | public _p1: SolverPoint; 75 | 76 | constructor() { 77 | this._p0 = new SolverPoint(0, 0); 78 | this._p1 = new SolverPoint(0, 0); 79 | } 80 | 81 | public get p0(): SolverPoint { 82 | return this._p0; 83 | } 84 | 85 | public get p1(): SolverPoint { 86 | return this._p1; 87 | } 88 | 89 | public get p0x(): number { 90 | return this._p0.x; 91 | } 92 | 93 | public get p0y(): number { 94 | return this._p0.y; 95 | } 96 | 97 | public get p1x(): number { 98 | return this._p1.x; 99 | } 100 | 101 | public get p1y(): number { 102 | return this._p1.y; 103 | } 104 | 105 | public get p0xIndex(): string { 106 | return this._p0.xIndex; 107 | } 108 | 109 | public get p0yIndex(): string { 110 | return this._p0.yIndex; 111 | } 112 | 113 | public get p1xIndex(): string { 114 | return this._p1.xIndex; 115 | } 116 | 117 | public get p1yIndex(): string { 118 | return this._p1.yIndex; 119 | } 120 | 121 | public setP0x(x: number) { 122 | this._p0.setX(x); 123 | } 124 | 125 | public setP0y(y: number) { 126 | this._p0.setY(y); 127 | } 128 | 129 | public setP1x(x: number) { 130 | this._p1.setX(x); 131 | } 132 | 133 | public setP1y(y: number) { 134 | this._p1.setY(y); 135 | } 136 | } 137 | 138 | export class SolverCircle implements SolverEntity { 139 | public _center: SolverPoint; 140 | public _radius: SolverScalar; 141 | 142 | constructor() { 143 | this._center = new SolverPoint(0, 0); 144 | this._radius = new SolverScalar(0); 145 | } 146 | 147 | public get center(): SolverPoint { 148 | return this._center; 149 | } 150 | 151 | public get radius(): number { 152 | return this._radius.value; 153 | } 154 | 155 | public get centerX(): number { 156 | return this._center.x; 157 | } 158 | 159 | public get centerY(): number { 160 | return this._center.y; 161 | } 162 | 163 | public get radiusIndex(): string { 164 | return this._radius.index; 165 | } 166 | 167 | public setCenterX(x: number) { 168 | this._center.setX(x); 169 | } 170 | 171 | public setCenterY(y: number) { 172 | this._center.setY(y); 173 | } 174 | 175 | public setRadius(radius: number) { 176 | this._radius.value = radius; 177 | } 178 | } -------------------------------------------------------------------------------- /src/math/point2.ts: -------------------------------------------------------------------------------- 1 | import VError from "verror"; 2 | 3 | import { Vector2D } from "./vector2"; 4 | import { 5 | isOverflow 6 | } from "./numeric"; 7 | 8 | /// A 2-dimensional mathematical point. 9 | /// 10 | /// Points are more contextual because of their need to reference the origin. 11 | export class Point2D { 12 | public static ORIGIN_2D: Point2D = new Point2D(0.0, 0.0); 13 | public static POINT_AT_INFINITY_2D: Point2D = new Point2D(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY); 14 | 15 | private _x: number; 16 | private _y: number; 17 | 18 | public constructor(x: number, y: number) { 19 | this._x = x; 20 | this._y = y; 21 | } 22 | 23 | public clone(): this { 24 | return new Point2D(this._x, this._y) as this; 25 | } 26 | 27 | public print(): string { 28 | return `({${this._x}}, {${this._y}})`; 29 | } 30 | 31 | public fromVector(v: Vector2D): this { 32 | return new Point2D(v.x, v.y) as this; 33 | } 34 | 35 | public get x(): number { 36 | return this._x; 37 | } 38 | 39 | public get y(): number { 40 | return this._y; 41 | } 42 | 43 | public setX(x: number): void { 44 | this._x = x; 45 | } 46 | 47 | public setY(y: number): void { 48 | this._y = y; 49 | } 50 | 51 | public set(x: number, y: number): void { 52 | this._x = x; 53 | this._y = y; 54 | } 55 | 56 | // Creates a displacement vector from the canonical origin. 57 | public asVector(): Vector2D { 58 | return new Vector2D(this._x, this._y); 59 | } 60 | 61 | /// Gets the distance between this point and a given point as a vector. 62 | public async vectorTo(p: this): Promise { 63 | let newX = p._x - this._x; 64 | let newY = p._y - this._y; 65 | if (isOverflow(newX) || isOverflow(newY)) { 66 | throw new VError("Point2D.vector_to(): overflowed result"); 67 | } 68 | return new Vector2D(newX, newY); 69 | } 70 | 71 | public isOrigin(): boolean { 72 | return this._x === Point2D.ORIGIN_2D._x && 73 | this._y === Point2D.ORIGIN_2D._y; 74 | } 75 | 76 | public isAtInfinity(): boolean { 77 | return Math.abs(this._x) === Point2D.POINT_AT_INFINITY_2D._x && 78 | Math.abs(this._y) === Point2D.POINT_AT_INFINITY_2D._y; 79 | } 80 | 81 | // Displaces the point by the given vector. 82 | public async add(v: Vector2D): Promise { 83 | const newX = this._x + v.x; 84 | const newY = this._y + v.y; 85 | if (isOverflow(newX) || isOverflow(newY)) { 86 | throw new VError("Point2D.add(): overflowed result"); 87 | } 88 | this._x = newX; 89 | this._y = newY; 90 | return this; 91 | } 92 | 93 | // Displaces the point by the negative of the given vector. 94 | public async sub(v: Vector2D): Promise { 95 | const newX = this._x - v.x; 96 | const newY = this._y - v.y; 97 | if (isOverflow(newX) || isOverflow(newY)) { 98 | throw new VError("Point2D.sub(): overflowed result"); 99 | } 100 | this._x = newX; 101 | this._y = newY; 102 | return this; 103 | } 104 | } 105 | 106 | // #[cfg(test)] 107 | // mod tests { 108 | // use super::*; 109 | // #[test] 110 | // fn point2d_create() { 111 | // let v = Point2D{x: 2.0, y: 3.0}; 112 | // assert_eq!(v.x, 2.0); 113 | // assert_eq!(v.y, 3.0); 114 | // } 115 | 116 | // #[test] 117 | // fn point2d_get_coordinates() { 118 | // let v = Point2D{x: 2.0, y: 3.0}; 119 | // let vcoords = v.get_coordinates(); 120 | // assert_eq!(vcoords.0, 2.0); 121 | // assert_eq!(vcoords.1, 3.0); 122 | // } 123 | 124 | // #[test] 125 | // fn point2d_as_vector() { 126 | // let p = Point2D{x: 2.0, y: 3.0}; 127 | // let v = p.as_vector(); 128 | // assert_eq!(p.x, v.x()); 129 | // assert_eq!(p.y, v.y()); 130 | // } 131 | 132 | // #[test] 133 | // fn point2d_add() { 134 | // let p = Point2D::new(2.0, 3.0); 135 | // let v = Vector2D::new(5.0, 7.0); 136 | // let res = p.add(v); 137 | // assert!(res.is_ok()); 138 | // assert_eq!(p.x, 2.0 + 5.0); 139 | // assert_eq!(p.y, 3.0 + 7.0); 140 | // } 141 | 142 | // #[test] 143 | // fn point2d_sub() { 144 | // let p = Point2D::new(5.0, 7.0); 145 | // let v = Vector2D::new(2.0, 3.0); 146 | // let res = p.sub(v); 147 | // assert!(res.is_ok()); 148 | // assert_eq!(p.x, 5.0 - 2.0); 149 | // assert_eq!(p.y, 7.0 - 3.0); 150 | // } 151 | // } -------------------------------------------------------------------------------- /src/math/point3.ts: -------------------------------------------------------------------------------- 1 | import VError from "verror"; 2 | 3 | import { Vector3D } from "./vector3"; 4 | import { 5 | isOverflow 6 | } from "./numeric"; 7 | 8 | /// A 3-dimensional mathematical point. 9 | /// 10 | /// Points are more contextual because of their need to reference an origin and they should be 11 | /// passed by reference whenever possible. 12 | export class Point3D { 13 | public static ORIGIN_3D: Point3D = new Point3D(0.0, 0.0, 0.0); 14 | public static POINT_AT_INFINITY_3D: Point3D = new Point3D(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY); 15 | 16 | private _x: number; 17 | private _y: number; 18 | private _z: number; 19 | 20 | public print(): string { 21 | return `({${this._x}}, {${this._y}}, {${this._z})`; 22 | } 23 | 24 | public constructor(x: number, y: number, z: number) { 25 | this._x = x; 26 | this._y = y; 27 | this._z = z; 28 | } 29 | 30 | public fromVector(v: Vector3D): this { 31 | return new Point3D(v.x, v.y, v.z) as this; 32 | } 33 | 34 | public clone(): this { 35 | return new Point3D(this._x, this._y, this._z) as this; 36 | } 37 | 38 | public get x(): number { 39 | return this._x; 40 | } 41 | 42 | public get y(): number { 43 | return this._y; 44 | } 45 | 46 | public get z(): number { 47 | return this._z; 48 | } 49 | 50 | public setX(x: number): void { 51 | this._x = x; 52 | } 53 | 54 | public setY(y: number): void { 55 | this._y = y; 56 | } 57 | 58 | public setZ(z: number): void { 59 | this._z = z; 60 | } 61 | 62 | public set(x: number, y: number, z: number): void { 63 | this._x = x; 64 | this._y = y; 65 | this._z = z; 66 | } 67 | 68 | public isOrigin(): boolean { 69 | return this._x === Point3D.ORIGIN_3D._x && 70 | this._y === Point3D.ORIGIN_3D._y && 71 | this._z === Point3D.ORIGIN_3D._z; 72 | } 73 | 74 | public isAtInfinity(): boolean { 75 | return Math.abs(this._x) === Point3D.POINT_AT_INFINITY_3D._x && 76 | Math.abs(this._y) === Point3D.POINT_AT_INFINITY_3D._y && 77 | Math.abs(this._z) === Point3D.POINT_AT_INFINITY_3D._z; 78 | } 79 | 80 | // Displaces the point by the given vector. 81 | public async add(v: Vector3D): Promise { 82 | const newX = this._x + v.x; 83 | const newY = this._y + v.y; 84 | const newZ = this._z + v.z; 85 | if (isOverflow(newX) || isOverflow(newY) || isOverflow(newZ)) { 86 | throw new VError("Point3D.add(): overflowed result"); 87 | } 88 | this.set(newX, newY, newZ); 89 | return this; 90 | } 91 | 92 | // Displaces the point by the negative of the given vector. 93 | public async sub(v: Vector3D): Promise { 94 | const newX = this._x - v.x; 95 | const newY = this._y - v.y; 96 | const newZ = this._z - v.z; 97 | if (isOverflow(newX) || isOverflow(newY) || isOverflow(newZ)) { 98 | throw new VError("Point3D.add(): overflowed result"); 99 | } 100 | this.set(newX, newY, newZ); 101 | return this; 102 | } 103 | 104 | // Creates a displacement vector from the canonical origin. 105 | public asVector(): Vector3D { 106 | return new Vector3D(this._x, this._y, this._z); 107 | } 108 | 109 | public async vectorTo(p: this): Promise { 110 | const newX = p._x - this._x; 111 | const newY = p._y - this._y; 112 | const newZ = p._z - this._z; 113 | if (isOverflow(newX) || isOverflow(newY) || isOverflow(newZ)) { 114 | throw new VError("Point3D.vector_to(): overflowed result"); 115 | } 116 | return new Vector3D(newX, newY, newZ); 117 | } 118 | } 119 | 120 | // #[cfg(test)] 121 | // mod tests { 122 | // use super::*; 123 | // #[test] 124 | // fn point3d_create() { 125 | // let p = Point3D{x: 2.0, y: 3.0, z: 5.0}; 126 | // assert_eq!(p.x, 2.0); 127 | // assert_eq!(p.y, 3.0); 128 | // assert_eq!(p.z, 5.0); 129 | // } 130 | 131 | // #[test] 132 | // fn point3d_get_coordinates() { 133 | // let p = Point3D{x: 2.0, y: 3.0, z: 5.0}; 134 | // let vcoords = p.get_coordinates(); 135 | // assert_eq!(vcoords.0, p.x()); 136 | // assert_eq!(vcoords.1, p.y()); 137 | // assert_eq!(vcoords.2, p.z()); 138 | // } 139 | 140 | // #[test] 141 | // fn point3d_as_vector() { 142 | // let p = Point3D{x: 2.0, y: 3.0, z: 5.0}; 143 | // let v = p.as_vector(); 144 | // assert_eq!(p.x, v.x()); 145 | // assert_eq!(p.y, v.y()); 146 | // assert_eq!(p.z, v.z()); 147 | // } 148 | 149 | // #[test] 150 | // fn point3d_add() { 151 | // let p = Point3D{x: 2.0, y: 3.0, z: 5.0}; 152 | // let v = Vector3D::new(7.0, 11.0, 13.0); 153 | // let res = p.add(v); 154 | // assert!(res.is_ok()); 155 | // assert_eq!(p.x, 2.0 + 7.0); 156 | // assert_eq!(p.y, 3.0 + 11.0); 157 | // assert_eq!(p.z, 5.0 + 13.0); 158 | // } 159 | 160 | // #[test] 161 | // fn point3d_sub() { 162 | // let p = Point3D{x: 7.0, y: 11.0, z: 13.0}; 163 | // let v = Vector3D::new(2.0, 3.0, 5.0); 164 | // let res = p.sub(v); 165 | // assert!(res.is_ok()); 166 | // assert_eq!(p.x, 7.0 - 2.0); 167 | // assert_eq!(p.y, 11.0 - 3.0); 168 | // assert_eq!(p.z, 13.0 - 5.0); 169 | // } 170 | // } -------------------------------------------------------------------------------- /src/solver/optim/lbfgs.ts: -------------------------------------------------------------------------------- 1 | import { VectorN } from "./nvector"; 2 | 3 | interface LBFGSSolverOptions { 4 | maxIterations?: number; 5 | tolerance?: number; 6 | } 7 | 8 | interface LBFGSSolverResult { 9 | success: boolean; 10 | iterations: number; 11 | objectiveValue: number; 12 | relativeDifference: number; 13 | } 14 | 15 | export class SolverLBFGS { 16 | private _size: number; 17 | private _steps: number; 18 | 19 | private _y: number[][]; 20 | private _s: number[][]; 21 | private _rho: number[]; 22 | 23 | private _tmp: number[]; 24 | 25 | public constructor(n: number, steps: number) { 26 | const M = Math.floor(steps); 27 | const N = Math.floor(n); 28 | 29 | // initialize data storage 30 | this._y = []; 31 | this._s = []; 32 | for (let i = 0; i < M; i++) { 33 | this._y[i] = VectorN.new(N); 34 | this._s[i] = VectorN.new(N); 35 | } 36 | this._rho = VectorN.new(N); 37 | 38 | // initialize temporary storage 39 | this._tmp = VectorN.new(N); 40 | 41 | // save parameters 42 | this._steps = M; 43 | this._size = N; 44 | } 45 | 46 | /** 47 | * Uses L-BFGS update described in Nocedal, 1980. 48 | * @param k The iteration number. 49 | * @param g The gradient at the current point. 50 | * @param x The current point. 51 | * @param d Modifies this vector in place. 52 | * @returns d. 53 | */ 54 | private _updateDirection(k: number, g: number[], x: number[], d: number[]): number[] { 55 | const m = this._steps; 56 | const iter = Math.floor(k); 57 | const incr = (iter <= m) ? 0 : (iter - m); 58 | const bound = (iter <= m) ? iter : m; 59 | 60 | const q = d; 61 | VectorN.copy(q, g); // q_bound = g_iter 62 | 63 | // compute latest s_k and y_k 64 | if (iter > 0) { 65 | const jj = (iter - 1) % m; // idx for s and y, this will be fetched this iteration 66 | const jj1 = iter % m; // idx for x and g; this will be stored for next iteration 67 | 68 | // assume x_k and g_k are already stored; calculate s_k and y_k 69 | VectorN.scaleAndAdd(x, -1.0, this._s[jj], this._s[jj]); 70 | VectorN.scaleAndAdd(g, -1.0, this._y[jj], this._y[jj]); 71 | 72 | // store x_k+1 and g_k+1 for later 73 | VectorN.copy(this._s[jj1], x); 74 | VectorN.copy(this._y[jj1], g); 75 | 76 | // compute rho_k 77 | this._rho[jj] = 1.0 / VectorN.dot(this._s[jj], this._y[jj]); 78 | } 79 | 80 | const alpha = VectorN.new(bound); 81 | for (let i = bound - 1; i >= 0; i--) { 82 | const j = i + incr; 83 | const sj = this._s[j % m]; 84 | const pj = this._rho[j % m]; 85 | const yj = this._y[j % m]; 86 | alpha[i] = pj * VectorN.dot(sj, q); 87 | VectorN.scaleAndAdd(q, -alpha[i], yj, q); 88 | } 89 | // r0 = H0 * q0; H0 = I 90 | for (let i = 0; i < bound; i++) { 91 | const j = i + incr; 92 | const yj = this._y[j % m]; 93 | const pj = this._rho[j % m]; 94 | const sj = this._s[j % m]; 95 | const beta_i = pj * VectorN.dot(yj, q); // beta_i = p_j * y_j^T * r_i 96 | VectorN.scaleAndAdd(q, alpha[i] - beta_i, sj, q); // r_i+1 = r_i + (alpha_i - beta_i) * s_j 97 | } 98 | // d_iter = r_bound = q 99 | VectorN.scale(-1.0, d, d); 100 | return d; 101 | } 102 | 103 | private _backtrackingArmijoLineSearch(x0: number[], f: (values: number[]) => number, g: number[], d: number[]): number { 104 | const alpha = 0.25; 105 | const beta = 0.5; 106 | let t = 1.0; 107 | 108 | const x = this._tmp; 109 | VectorN.scaleAndAdd(x0, t, d, x); 110 | let lhs = f(x); 111 | let rhs = f(x0) + alpha * t * VectorN.dot(g, d); 112 | while (lhs > rhs) { 113 | t *= beta; 114 | VectorN.scaleAndAdd(x0, t, d, x); 115 | lhs = f(x); 116 | rhs = f(x0) + alpha * t * VectorN.dot(g, d); 117 | } 118 | return t; 119 | } 120 | 121 | // Solves the unconstrained optimization problem using L-BFGS. 122 | public async solve(values: number[], f: (values: number[]) => number, df: (values: number[]) => number[], options?: LBFGSSolverOptions): Promise { 123 | let { 124 | maxIterations, 125 | tolerance 126 | } = options || {}; 127 | 128 | const MAX_ITER = maxIterations || 1000; 129 | const TOL = tolerance || 1e-10; 130 | 131 | let k = 0; 132 | let n = values.length; 133 | const x = VectorN.new(n); 134 | VectorN.copy(x, values); 135 | // const g = VectorN.new(n); 136 | // df(x, g); 137 | let g = df(x); 138 | 139 | // initialize direction vector 140 | let d = VectorN.new(values.length); 141 | 142 | // initialize x0 and g0 for updates 143 | VectorN.copy(this._s[0], x); 144 | VectorN.copy(this._y[0], g); 145 | 146 | const iter = 0; 147 | let fx0 = f(x); 148 | let fx1 = Number.MAX_SAFE_INTEGER; 149 | while (iter < MAX_ITER) { 150 | d = this._updateDirection(k, g, x, d); 151 | const a = this._backtrackingArmijoLineSearch(x, f, g, d); 152 | VectorN.scaleAndAdd(x, a, d, x); // x1 = x0 + a * d 153 | fx1 = f(x); 154 | g = df(x); 155 | if (Math.abs(fx1 - fx0) < TOL) { 156 | VectorN.copy(values, x); 157 | return { 158 | success: true, 159 | iterations: iter, 160 | objectiveValue: fx1, 161 | relativeDifference: Math.abs(fx1 - fx0), 162 | }; 163 | } 164 | // reset loop 165 | k++; 166 | fx0 = fx1; 167 | } 168 | return { 169 | success: false, 170 | iterations: iter, 171 | objectiveValue: fx1, 172 | relativeDifference: Math.abs(fx1 - fx0), 173 | }; 174 | } 175 | } -------------------------------------------------------------------------------- /src/geometry/circle2.ts: -------------------------------------------------------------------------------- 1 | import VError from "verror"; 2 | 3 | import { Point2D } from "src/math/point2"; 4 | import { Vector2D } from "src/math/vector2"; 5 | import { Line2D } from "./line2"; 6 | import { 7 | isOverflow, 8 | } from "../math/numeric"; 9 | 10 | export class Circle2D { 11 | private _center: Point2D; 12 | private _radius: number; 13 | 14 | private constructor(center: Point2D, radius: number) { 15 | this._center = center; 16 | this._radius = radius; 17 | } 18 | 19 | /// Constructs a Circle2D from a center point and a radius. 20 | public static async create(center: Point2D, radius: number): Promise { 21 | if (Math.abs(radius) === Number.POSITIVE_INFINITY) { 22 | throw new VError("Circle2D.new(): radius is infinite"); 23 | } 24 | if (isNaN(radius)) { 25 | throw new VError("Circle2D.new(): radius is NaN"); 26 | } 27 | if (radius < 0.0) { 28 | throw new VError("Circle2D.new(): radius is negative"); 29 | } 30 | return new Circle2D(center.clone(), radius); 31 | } 32 | 33 | /// Constructs a Circle2D from a center point and a point on the circle. 34 | public static async newFromCenterAndPoint(center_point: Point2D, p: Point2D): Promise { 35 | const v = await center_point.vectorTo(p); 36 | const r = await v.length(); 37 | return new Circle2D(center_point.clone(), r); 38 | } 39 | 40 | public static async newFrom3Points(p1: Point2D, p2: Point2D, p3: Point2D, tol: number): Promise { 41 | // test for collinearity 42 | const v12 = await p1.vectorTo(p2); 43 | const v13 = await p1.vectorTo(p3); 44 | 45 | const l12 = await Line2D.create(p1, v12); 46 | const isPointOnL12 = await l12.isPointOn(p3, tol); 47 | if (isPointOnL12) { 48 | throw new VError("CircularArc2D.from_3_points(): arc points are collinear"); 49 | } 50 | 51 | // calculate center from construction 52 | let v12_mid; 53 | try { 54 | v12_mid = await v12.scale(0.5); 55 | } catch (e) { 56 | throw new VError(e as Error, "CircularArc2D.from_3_points(): could not get midpoint between p1-p2"); 57 | } 58 | let v13_mid; 59 | try { 60 | v13_mid = await v13.scale(0.5); 61 | } catch (e) { 62 | throw new VError(e as Error, "CircularArc2D.from_3_points(): could not get midpoint between p1-p3"); 63 | } 64 | 65 | const l12_mid = new Point2D(p1.x + v12_mid.x, p1.y + v12_mid.y); 66 | const l13_mid = new Point2D(p1.x + v13_mid.x, p1.y + v13_mid.y); 67 | 68 | const p12_mid_perp = v12.getPerpendicular(); 69 | const p13_mid_perp = v13.getPerpendicular(); 70 | 71 | const l12_p = await Line2D.create(l12_mid, p12_mid_perp); 72 | const l13_p = await Line2D.create(l13_mid, p13_mid_perp); 73 | 74 | const center = l12_p.intersectsLine(l13_p); 75 | 76 | // get circle radius 77 | const vr = await center.vectorTo(p1); 78 | let r; 79 | try { 80 | r = await vr.length(); 81 | } catch (e) { 82 | throw new VError(e as Error, "CircularArc2D.from_3_points(): could not get radius between center and p1"); 83 | } 84 | return new Circle2D(center, r); 85 | } 86 | 87 | /// Constructs a Circle2D from the coordinates of the center point and the radius. 88 | public static async newFromCoordinates(x: number, y: number, radius: number): Promise { 89 | if (isOverflow(x) || isOverflow(y) || isOverflow(radius)) { 90 | throw new VError("Circle2D.newFromCoordinates(): overflowed result"); 91 | } 92 | return new Circle2D(new Point2D(x, y), radius); 93 | } 94 | 95 | public getCenter(): Point2D { 96 | return this._center.clone(); 97 | } 98 | 99 | public getRadius(): number { 100 | return this._radius; 101 | } 102 | 103 | public async isPointOn(p: Point2D, tol: number): Promise { 104 | if (Math.abs(this._radius) < 1e-14) { 105 | throw new VError("Circle2D.isPointOn: radius is too small") 106 | } 107 | 108 | // calculate point on 109 | let cx = this._center.x; 110 | let cy = this._center.y; 111 | let r = this._radius; 112 | let px = p.x; 113 | let py = p.y; 114 | 115 | // calculate a point on the circle closest to p 116 | let dx = px - cx; 117 | let dy = py - cy; 118 | let diff = new Vector2D(dx, dy); 119 | let norm_diff = await diff.normalize(); 120 | let direction_to_p = norm_diff; 121 | 122 | let c_to_p = await direction_to_p.scale(r); 123 | let p_prime = this._center.clone(); 124 | await p_prime.add(c_to_p); 125 | 126 | // determine distance from p to p' 127 | let dp = await p_prime.vectorTo(p); 128 | let dr = await dp.length(); 129 | 130 | return (dr <= tol); 131 | } 132 | } 133 | 134 | // #[cfg(test)] 135 | // mod tests { 136 | // use super::*; 137 | 138 | // #[test] 139 | // fn circle2d_get_radius() { 140 | // let unit_rad = 1.0; 141 | // let center = Point2D::new(0.0, 0.0); 142 | // let c = Circle2D { 143 | // center: center, 144 | // radius: unit_rad 145 | // }; 146 | 147 | // let r = c.get_radius(); 148 | // assert_eq!(r, unit_rad); 149 | // } 150 | 151 | // #[test] 152 | // fn circle2d_is_point_on() { 153 | // let center = Point2D::new(0.0, 0.0); 154 | // let c = Circle2D { 155 | // center: center, 156 | // radius: 1.0 157 | // }; 158 | 159 | // let p = Point2D::new(1.0 + 1e-13, 0.0); 160 | // let res = c.is_point_on(p, 1e-14); 161 | // assert!(res.is_ok()); 162 | // assert_eq!(res.unwrap(), false); 163 | 164 | // let p = Point2D::new(1.0 + 1e-14, 0.0); 165 | // let res = c.is_point_on(p, 1e-14); 166 | // assert!(res.is_ok()); 167 | // assert_eq!(res.unwrap(), true); 168 | // } 169 | // } -------------------------------------------------------------------------------- /src/math/matrix2.ts: -------------------------------------------------------------------------------- 1 | import VError from "verror"; 2 | 3 | import { Vector2D } from "./vector2"; 4 | import { 5 | isOverflow, 6 | isValidTolerance, 7 | } from "./numeric"; 8 | 9 | // Row-major 2x2 Matrix. 10 | export class Matrix2D { 11 | // Creates a new identity matrix from the given elements. 12 | public static identity(): Matrix2D { 13 | return new Matrix2D([1, 0, 0, 1]); 14 | } 15 | 16 | private _elements: [number, number, number, number]; 17 | 18 | constructor(elements: number[]) { 19 | this._elements = elements.slice(0, 4) as [number, number, number, number]; 20 | } 21 | 22 | public get a00(): number { 23 | return this._elements[0]; 24 | } 25 | 26 | public get a01(): number { 27 | return this._elements[1]; 28 | } 29 | 30 | public get a10(): number { 31 | return this._elements[2]; 32 | } 33 | 34 | public get a11(): number { 35 | return this._elements[3]; 36 | } 37 | 38 | // Pretty-prints the matrix. 39 | public print(): string { 40 | return `([{${this._elements[0]}}, {${this._elements[1]}}], [{${this._elements[2]}}, {${this._elements[3]}}])`; 41 | } 42 | 43 | // Creates a new matrix from the given elements. 44 | public clone(): this { 45 | return new Matrix2D(this._elements) as this; 46 | } 47 | 48 | // Adds the given matrix to this matrix in-place. 49 | public add(w: this): void { 50 | this._elements[0] += w._elements[0]; 51 | this._elements[1] += w._elements[1]; 52 | this._elements[2] += w._elements[2]; 53 | this._elements[3] += w._elements[3]; 54 | } 55 | 56 | // Subtracts the given matrix from this matrix in-place. 57 | public sub(w: this): void { 58 | this._elements[0] -= w._elements[0]; 59 | this._elements[1] -= w._elements[1]; 60 | this._elements[2] -= w._elements[2]; 61 | this._elements[3] -= w._elements[3]; 62 | } 63 | 64 | // Computes the product of two matrices and stores the result in the output matrix. 65 | private mul(left: this, right: this, output: this): this { 66 | const a = left._elements[0]; 67 | const b = left._elements[1]; 68 | const c = left._elements[2]; 69 | const d = left._elements[3]; 70 | 71 | const e = right._elements[0]; 72 | const f = right._elements[1]; 73 | const g = right._elements[2]; 74 | const h = right._elements[3]; 75 | 76 | output._elements[0] = a * e + b * g; 77 | output._elements[1] = a * f + b * h; 78 | output._elements[2] = c * e + d * g; 79 | output._elements[3] = c * f + d * h; 80 | 81 | return output; 82 | } 83 | 84 | // Multiplies this matrix on the left by the given matrix and returns the result (result = w * this). 85 | public lmul(w: this): this { 86 | return this.mul(w, this, this); 87 | } 88 | 89 | // Multiplies this matrix on the right by the given matrix and returns the result (result = this * w). 90 | public rmul(w: this): this { 91 | return this.mul(this, w, this); 92 | } 93 | 94 | // Sets the matrix to the identity matrix in-place. 95 | public toIdentity(): void { 96 | this._elements = [1.0, 0.0, 0.0, 1.0]; 97 | } 98 | 99 | // Transposes the matrix in-place. 100 | public transpose(): void { 101 | const tmp = this._elements[1]; 102 | this._elements[1] = this._elements[2]; 103 | this._elements[2] = tmp; 104 | } 105 | 106 | // Sets the matrix to the adjugate in-place and returns it. 107 | public adjugate(): this { 108 | const [a, b, c, d] = this._elements; 109 | this._elements[0] = d; 110 | this._elements[1] = -b; 111 | this._elements[2] = -c; 112 | this._elements[3] = a; 113 | return this; 114 | } 115 | 116 | // Gets the determinant of the matrix. 117 | public determinant(): number { 118 | const [a, b, c, d] = this._elements; 119 | return a * d - b * c; 120 | } 121 | 122 | // Gets the row of this matrix at the given index as a vector. 123 | public async getRow(row: number): Promise { 124 | switch (row) { 125 | case 0: 126 | return new Vector2D(this._elements[0], this._elements[1]); 127 | case 1: 128 | return new Vector2D(this._elements[2], this._elements[3]); 129 | default: 130 | throw new VError("Matrix2D.getRow: index should be 0 or 1"); 131 | } 132 | } 133 | 134 | // Gets the column of this matrix at the given index as a vector. 135 | public async getCol(col: number): Promise { 136 | switch (col) { 137 | case 0: 138 | return new Vector2D(this._elements[0], this._elements[2]); 139 | case 1: 140 | return new Vector2D(this._elements[1], this._elements[3]); 141 | default: 142 | throw new VError("Matrix2D.getCol: index should be 0 or 1"); 143 | } 144 | } 145 | 146 | /// Multiplies this matrix by the given scalar value. 147 | public async scale(s: number): Promise { 148 | if (isNaN(s)) { 149 | throw new VError("Matrix2D.scale: scale factor is NaN"); 150 | } 151 | let e0 = this._elements[0] * s; 152 | let e1 = this._elements[1] * s; 153 | let e2 = this._elements[2] * s; 154 | let e3 = this._elements[3] * s; 155 | if (isOverflow(e0) || isOverflow(e1) || isOverflow(e2) || isOverflow(e3)) { 156 | throw new VError("Matrix2D.scale: overflowed element"); 157 | } 158 | return this; 159 | } 160 | 161 | // Inverts this matrix in-place and returns it. Throws an error if the determinant is zero. 162 | public async invert(): Promise { 163 | const det = this.determinant(); 164 | if (det === 0.0) { 165 | throw new Error("Matrix2D.invert: determinant is zero"); 166 | } 167 | return this.adjugate().scale(1.0 / det); 168 | } 169 | 170 | // Returns true if the matrix is singular within the given tolerance (determinant < tol). 171 | public async isSingular(tol: number): Promise { 172 | if (!isValidTolerance(tol)) { 173 | throw new VError("Matrix2D.isSingular: invalid tolerance"); 174 | } 175 | 176 | let det = this.determinant(); 177 | return Math.abs(det) < tol; 178 | } 179 | 180 | // Modifies the vector in-place and returns it. 181 | public async applyToVector(v: Vector2D): Promise { 182 | const a = this._elements[0]; 183 | const b = this._elements[1]; 184 | const newX = a * v.x + b * v.y; 185 | const c = this._elements[2]; 186 | const d = this._elements[3]; 187 | const newY = c * v.x + d * v.y; 188 | if (isOverflow(newX) || isOverflow(newY)) { 189 | throw new VError("Matrix2D.applyToVector: overflowed result"); 190 | } 191 | v.set(newX, newY); 192 | return v; 193 | } 194 | } 195 | 196 | // #[cfg(test)] 197 | // mod tests { 198 | // use super::*; 199 | 200 | // #[test] 201 | // fn matrix2d_create() { 202 | // let elems: [f64; 4] = [2.0, 3.0, 5.0, 7.0]; 203 | // let v = Matrix2D{ elements: elems }; 204 | // assert_eq!(v.elements[0], 2.0); 205 | // assert_eq!(v.elements[1], 3.0); 206 | // assert_eq!(v.elements[2], 5.0); 207 | // assert_eq!(v.elements[3], 7.0); 208 | // } 209 | 210 | // #[test] 211 | // fn matrix2d_identity() { 212 | // let v = Matrix2D::identity(); 213 | // assert_eq!(v.elements[0], 1.0); 214 | // assert_eq!(v.elements[1], 0.0); 215 | // assert_eq!(v.elements[2], 0.0); 216 | // assert_eq!(v.elements[3], 1.0); 217 | // } 218 | 219 | // #[test] 220 | // fn matrix2d_transpose() { 221 | // let elems: [f64; 4] = [2.0, 3.0, 5.0, 7.0]; 222 | // let v = Matrix2D{ elements: elems }; 223 | // let vt = v.transpose(); 224 | 225 | // assert_eq!(v.elements[0], vt.elements[0]); 226 | // assert_eq!(v.elements[1], vt.elements[2]); 227 | // assert_eq!(v.elements[2], vt.elements[1]); 228 | // assert_eq!(v.elements[3], vt.elements[3]); 229 | // } 230 | 231 | // #[test] 232 | // fn matrix2d_get_row() { 233 | // let elems: [f64; 4] = [2.0, 3.0, 5.0, 7.0]; 234 | // let v = Matrix2D{ elements: elems }; 235 | 236 | // let r1 = v.get_row(0); 237 | // assert!(r1.is_ok()); 238 | // let r1 = r1.unwrap(); 239 | // assert_eq!(r1.x(), v.elements[0]); 240 | // assert_eq!(r1.y(), v.elements[1]); 241 | 242 | // let r2 = v.get_row(1); 243 | // assert!(r2.is_ok()); 244 | // let r2 = r2.unwrap(); 245 | // assert_eq!(r2.x(), v.elements[2]); 246 | // assert_eq!(r2.y(), v.elements[3]); 247 | 248 | // let r3 = v.get_row(3); 249 | // assert!(r3.is_err()); 250 | // } 251 | 252 | // #[test] 253 | // fn matrix2d_get_col() { 254 | // let elems: [f64; 4] = [2.0, 3.0, 5.0, 7.0]; 255 | // let v = Matrix2D{ elements: elems }; 256 | 257 | // let r1 = v.get_col(0); 258 | // assert!(r1.is_ok()); 259 | // let r1 = r1.unwrap(); 260 | // assert_eq!(r1.x(), v.elements[0]); 261 | // assert_eq!(r1.y(), v.elements[2]); 262 | 263 | // let r2 = v.get_col(1); 264 | // assert!(r2.is_ok()); 265 | // let r2 = r2.unwrap(); 266 | // assert_eq!(r2.x(), v.elements[1]); 267 | // assert_eq!(r2.y(), v.elements[3]); 268 | 269 | // let r3 = v.get_row(3); 270 | // assert!(r3.is_err()); 271 | // } 272 | 273 | // #[test] 274 | // fn matrix2d_singular() { 275 | // let sing_elems: [f64; 4] = [2.0, 1.0, 4.0, 2.0]; 276 | // let v_singular = Matrix2D{ elements: sing_elems }; 277 | // let res = v_singular.is_singular(1e-14); 278 | // assert!(res.is_ok()); 279 | // assert_eq!(res.unwrap(), true); 280 | 281 | // let elems: [f64; 4] = [2.0, 1.0, 4.0, 5.0]; 282 | // let v = Matrix2D{ elements: elems }; 283 | // let res = v.is_singular(1e-14); 284 | // assert!(res.is_ok()); 285 | // assert_eq!(res.unwrap(), false); 286 | // } 287 | 288 | // #[test] 289 | // fn matrix2d_scale() { 290 | // let elems: [f64; 4] = [2.0, 3.0, 5.0, 7.0]; 291 | // let v = Matrix2D{ elements: elems }; 292 | // let s = 4.0; 293 | // let res = v.scale(s); 294 | // assert!(res.is_ok()); 295 | // let m = res.unwrap(); 296 | // assert_eq!(m.elements[0], v.elements[0] * s); 297 | // assert_eq!(m.elements[1], v.elements[1] * s); 298 | // assert_eq!(m.elements[2], v.elements[2] * s); 299 | // assert_eq!(m.elements[3], v.elements[3] * s); 300 | // } 301 | 302 | // #[test] 303 | // fn matrix2d_apply_to_vector() { 304 | // let elems: [f64; 4] = [2.0, 3.0, 5.0, 7.0]; 305 | // let m = Matrix2D{ elements: elems }; 306 | // let v = Vector2D::new(11.0, 13.0 ); 307 | 308 | // let res = m.apply_to_vector(v); 309 | // assert!(res.is_ok()); 310 | // let w = res.unwrap(); 311 | // assert_eq!(w.x(), 2.0 * 11.0 + 3.0 * 13.0); 312 | // assert_eq!(w.y(), 5.0 * 11.0 + 7.0 * 13.0); 313 | // } 314 | // } -------------------------------------------------------------------------------- /src/geometry/line2.ts: -------------------------------------------------------------------------------- 1 | import VError from 'verror'; 2 | 3 | import { Point2D } from '../math/point2'; 4 | import { Vector2D } from '../math/vector2'; 5 | import { Vector3D } from '../math/vector3'; 6 | 7 | /// Constructed by a point and a unit direction. 8 | export class Line2D { 9 | private point: Point2D; 10 | private direction: Vector2D; 11 | 12 | private constructor(point: Point2D, direction: Vector2D) { 13 | this.point = point; 14 | this.direction = direction; 15 | } 16 | 17 | public getPoint(): Point2D { 18 | return this.point.clone(); 19 | } 20 | 21 | public getDirection(): Vector2D { 22 | return this.direction.clone(); 23 | } 24 | 25 | public static async create(point: Point2D, direction: Vector2D): Promise { 26 | const d = await direction.normalize(); 27 | return new Line2D(point.clone(), d); 28 | } 29 | 30 | public static async newFrom2Points(start: Point2D, end: Point2D): Promise { 31 | let d = await start.vectorTo(end); 32 | return Line2D.create(start, d); 33 | } 34 | 35 | /// Defines a line from an ordered triple that represents the coefficients in the equation ax + by + c = 0. 36 | public static async newFromTriple(a: number, b: number, c: number): Promise { 37 | let d = new Vector2D(b, -a); 38 | if (a != 0.0) { 39 | let p = new Point2D(-c / a, 0.0); 40 | return Line2D.create(p, d); 41 | } else if (b !== 0.0) { 42 | let p = new Point2D(0.0, -c / b); 43 | return Line2D.create(p, d); 44 | } else { 45 | // a === 0 and b === 0 46 | let origin = new Point2D(0.0, 0.0); 47 | return Line2D.create(origin, d); 48 | } 49 | } 50 | 51 | /// Calculates the intersection between this line and a given line. 52 | /// Returns (Inf, Inf) if the lines are parallel (i.e. intersects at infinity). 53 | public intersectsLine(line: Line2D): Point2D { 54 | let thisTriple = this.getTriple(); 55 | let lineTriple = line.getTriple(); 56 | 57 | let homIntersect = thisTriple.cross(lineTriple); 58 | 59 | let x = homIntersect.x; 60 | let y = homIntersect.y; 61 | let w = homIntersect.z; 62 | 63 | if (w === 0.0) { 64 | return Point2D.POINT_AT_INFINITY_2D.clone(); 65 | } else { 66 | return new Point2D(x / w, y / w); 67 | } 68 | } 69 | 70 | /// True if the point is at the origin and the direction is a zero-length vector, false if not. 71 | public isZeroLengthLine(): boolean { 72 | return this.point.x === 0.0 && this.point.y === 0.0 && 73 | this.direction.x === 0.0 && this.direction.y === 0.0; 74 | } 75 | 76 | /// Gets the triple (a, b, c) that represents the implicit equation ax + by + c = 0. 77 | /// This is a Vector3D for implicit intersection calculations. 78 | public getTriple(): Vector3D { 79 | let x0 = this.point.x; 80 | let y0 = this.point.y; 81 | let u = this.direction.x; 82 | let v = this.direction.y; 83 | return new Vector3D(-v, u, v * x0 - u * y0); 84 | } 85 | 86 | /// Determines if the given point is on the line, true if yes, false if no. 87 | public async isPointOn(p: Point2D, tol: number): Promise { 88 | let triple = this.getTriple(); 89 | let x = p.x; 90 | let y = p.y; 91 | let homP = new Vector3D(x, y, 1.0); 92 | const d = await triple.dot(homP); 93 | return (Math.abs(d) < tol); 94 | } 95 | 96 | /// Calculates the point as a function of the parameter t with the formula P = (point + direction * t). 97 | public getPointFromParameter(t: number): Point2D { 98 | let u = this.direction.x; 99 | let v = this.direction.y; 100 | let x0 = this.point.x; 101 | let y0 = this.point.y; 102 | return new Point2D(x0 + t * u, y0 + t * v); 103 | } 104 | 105 | /// Gets the closest point on the line to the given point. 106 | public async getClosestPointTo(q: Point2D, tol: number): Promise { 107 | let q_p0 = await this.getPoint().vectorTo(q); 108 | let n = this.getDirection().clone(); 109 | let t = await n.dot(q_p0); 110 | 111 | let p = this.getPointFromParameter(t); 112 | if (await this.isPointOn(p, tol)) { 113 | return p; 114 | } else { 115 | throw new VError("InfiniteLine2D.get_closest_point_to: cannot get point on line within tolerance"); 116 | } 117 | } 118 | 119 | /// Gets the perpendicular line to this one that passes through the point p. 120 | public getPerpLine(p: Point2D): Line2D { 121 | let u = this.direction.x; 122 | let v = this.direction.y; 123 | return new Line2D(p.clone(), new Vector2D(-v, u)); 124 | } 125 | 126 | /// Determines if the given line is collinear to this one within a given tolerance. 127 | public async isCollinearTo(line: Line2D, tol: number): Promise { 128 | let lp = line.point; 129 | let onThis; 130 | try { 131 | onThis = await this.isPointOn(lp, tol); 132 | } catch (e) { 133 | throw new VError(e as Error, "InfiniteLine2D.is_collinear_to: cannot determine point on line status") 134 | } 135 | 136 | let colThis; 137 | try { 138 | colThis = await this.direction.isParallelTo(line.direction, tol) 139 | } catch (e) { 140 | throw new VError("InfiniteLine2D.is_collinear_to: cannot determine direction collinearity status") 141 | } 142 | return (onThis && colThis); 143 | } 144 | 145 | /// Determines if the given line is parallel to this one within a given tolerance. 146 | public async isParallelTo(line: Line2D, tol: number): Promise { 147 | return line.direction.isParallelTo(this.direction, tol); 148 | } 149 | 150 | /// Determines if the given line is perpendicular to this one within a given tolerance. 151 | public is_perpendicular_to(line: Line2D, tol: number): Promise { 152 | return this.direction.isPerpendicularTo(line.direction, tol); 153 | } 154 | } 155 | 156 | // #[cfg(test)] 157 | // mod tests { 158 | // use crate::linalg::{Point2D, Vector2D}; 159 | // use super::*; 160 | 161 | // #[test] 162 | // fn infinite_line2d_get_triple() { 163 | // let line = Line2D{ 164 | // point: Point2D::new(-1.0, -1.0 ), 165 | // direction: Vector2D::new(2.0, -1.0 ) 166 | // }; 167 | // let triple = line.get_triple(); 168 | // assert_eq!(triple.x(), 1.0); 169 | // assert_eq!(triple.y(), 2.0); 170 | // assert_eq!(triple.z(), 3.0); 171 | // } 172 | 173 | // #[test] 174 | // fn infinite_line2d_from_triple() { 175 | // let x = 1.0; 176 | // let y = 2.0; 177 | // let z = 3.0; 178 | // let line = Line2D::new_from_triple(x, y, z); 179 | // assert!(line.is_ok()); 180 | // let triple = line.unwrap().get_triple(); 181 | // assert_eq!(triple.x(), x); 182 | // assert_eq!(triple.y(), y); 183 | // assert_eq!(triple.z(), z); 184 | // } 185 | 186 | // #[test] 187 | // fn infinite_line2d_zero_length_line() { 188 | // let x = 0.0; 189 | // let y = 0.0; 190 | // let z = 0.0; 191 | // let line = Line2D::new_from_triple(x, y, z); 192 | // assert!(line.is_ok()); 193 | // let line = line.unwrap(); 194 | // assert!(line.is_zero_length_line()); 195 | 196 | // let line = Line2D::new_from_triple(1e-14, y, z); 197 | // assert!(line.is_ok()); 198 | // let line = line.unwrap(); 199 | // assert!(!line.is_zero_length_line()); 200 | // } 201 | 202 | // #[test] 203 | // fn infinite_line2d_get_perp_line() { 204 | // let line = Line2D{ 205 | // point: Point2D::new(1.0, 1.0 ), 206 | // direction: Vector2D::new(-3.0, 7.0 ) 207 | // }; 208 | // let q = Point2D::new(2.0, 5.0 ); 209 | // let perp = line.get_perp_line(q); 210 | 211 | // assert_eq!(perp.point.x(), q.x()); 212 | // assert_eq!(perp.point.y(), q.y()); 213 | // assert_eq!(perp.direction.x(), -line.direction.y()); 214 | // assert_eq!(perp.direction.y(), line.direction.x()); 215 | // } 216 | 217 | // #[test] 218 | // fn infinite_line2d_parallel_to() { 219 | // // obviously parallel 220 | // let line1 = Line2D{ 221 | // point: Point2D::new(0.0, 0.0 ), 222 | // direction: Vector2D::new(1.0, 0.0 ) 223 | // }; 224 | // let line2 = Line2D{ 225 | // point: Point2D::new(0.0, 1.0 ), 226 | // direction: Vector2D::new(-1.0, 0.0 ) 227 | // }; 228 | // let res1 = line1.is_parallel_to(line2, 1e-14); 229 | // assert!(res1.is_ok()); 230 | // let res1 = res1.unwrap(); 231 | // assert_eq!(res1, true); 232 | 233 | // // obviously not parallel 234 | // let line3 = Line2D{ 235 | // point: Point2D::new(0.0, 1.0 ), 236 | // direction: Vector2D::new(1.0, 1.0 ) 237 | // }; 238 | // let res2 = line1.is_parallel_to(line3, 1e-14); 239 | // assert!(res2.is_ok()); 240 | // let res2 = res2.unwrap(); 241 | // assert_eq!(res2, false); 242 | 243 | // // slightly parallel 244 | // let line3 = Line2D{ 245 | // point: Point2D::new(0.0, 1.0 ), 246 | // direction: Vector2D::new(1.0, 5e-14 ) 247 | // }; 248 | // let res2 = line1.is_parallel_to(line3, 1e-13); 249 | // assert!(res2.is_ok()); 250 | // let res2 = res2.unwrap(); 251 | // assert_eq!(res2, true); 252 | // } 253 | 254 | // #[test] 255 | // fn infinite_line2d_collinear_to() { 256 | // let line1 = Line2D{ 257 | // point: Point2D::new(0.0, 0.0 ), 258 | // direction: Vector2D::new(1.0, 0.0 ) 259 | // }; 260 | // let line2 = Line2D{ 261 | // point: Point2D::new(2.0, 0.0 ), 262 | // direction: Vector2D::new(-1.0, 0.0 ) 263 | // }; 264 | // let res1 = line1.is_collinear_to(line2, 1e-14); 265 | // assert!(res1.is_ok()); 266 | // let res1 = res1.unwrap(); 267 | // assert_eq!(res1, true); 268 | 269 | // let line3 = Line2D{ 270 | // point: Point2D::new(2.0, 1e-14 ), 271 | // direction: Vector2D::new(-1.0, 0.0 ) 272 | // }; 273 | // let res2 = line1.is_collinear_to(line3, 1e-14); 274 | // assert!(res2.is_ok()); 275 | // let res2 = res2.unwrap(); 276 | // assert_eq!(res2, false); 277 | // } 278 | 279 | // #[test] 280 | // fn infinite_line2d_perpendicular_to() { 281 | // // obviously perpendicular 282 | // let line1 = Line2D{ 283 | // point: Point2D::new(0.0, 0.0 ), 284 | // direction: Vector2D::new(1.0, 0.0 ) 285 | // }; 286 | // let line2 = Line2D{ 287 | // point: Point2D::new(2.0, 0.0 ), 288 | // direction: Vector2D::new(0.0, -1.0 ) 289 | // }; 290 | // let res = line1.is_perpendicular_to(line2, 1e-14); 291 | // assert!(res.is_ok()); 292 | // let res = res.unwrap(); 293 | // assert_eq!(res, true); 294 | 295 | // // slightly not perpendicular 296 | // let line3 = Line2D{ 297 | // point: Point2D::new(2.0, 0.0 ), 298 | // direction: Vector2D::new(1.5e-14, -1.0 ) 299 | // }; 300 | // let res = line1.is_perpendicular_to(line3, 1e-14); 301 | // assert!(res.is_ok()); 302 | // let res = res.unwrap(); 303 | // assert_eq!(res, false); 304 | // } 305 | // } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 83 | 84 | /* Type Checking */ 85 | "strict": true, /* Enable all strict type-checking options. */ 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | }, 109 | "include": [ 110 | "src/**/*" 111 | ], 112 | "exclude": [ 113 | "node_modules", 114 | "dist" 115 | ] 116 | } 117 | -------------------------------------------------------------------------------- /src/math/vector2.ts: -------------------------------------------------------------------------------- 1 | import { VError } from "verror"; 2 | 3 | import { 4 | twoProduct, 5 | twoSum, 6 | isOverflow, 7 | isValidTolerance, 8 | resultFromCalculation, 9 | nrm2, 10 | inscribedAngleTriangle 11 | } from "./numeric"; 12 | 13 | /// A 2-dimensional mathematical vector. 14 | /// 15 | /// Since points are more contextual because of their need to reference an origin, they should be 16 | /// passed by reference whenever possible. Vectors should be value-based and copied when possible. 17 | export class Vector2D { 18 | public static ZERO_2D = new Vector2D(0.0, 0.0); 19 | public static XAXIS_2D = new Vector2D(1.0, 0.0); 20 | public static YAXIS_2D = new Vector2D(0.0, 1.0); 21 | 22 | private _x: number; 23 | private _y: number; 24 | 25 | constructor(x: number, y: number) { 26 | this._x = x; 27 | this._y = y; 28 | } 29 | 30 | public get x(): number { 31 | return this._x; 32 | } 33 | 34 | public get y(): number { 35 | return this._y; 36 | } 37 | 38 | public setX(x: number): void { 39 | this._x = x; 40 | } 41 | 42 | public setY(y: number): void { 43 | this._y = y; 44 | } 45 | 46 | public set(x: number, y: number): void { 47 | this._x = x; 48 | this._y = y; 49 | } 50 | 51 | public print(): string { 52 | return `({${this._x}}, {${this._y}})`; 53 | } 54 | 55 | public clone(): this { 56 | return new Vector2D(this._x, this._y) as this; 57 | } 58 | 59 | public async dot(w: Vector2D): Promise { 60 | // let d = (self.x * w.x) + (self.y * w.y); 61 | let x1 = this._x; 62 | let x2 = this._y; 63 | let y1 = w._x; 64 | let y2 = w._y; 65 | 66 | let [p, s] = twoProduct(x1, y1); 67 | // for i in 2..3 { 68 | let [h, r] = twoProduct(x2, y2); 69 | let [pp, qq] = twoSum(p, h); 70 | p = pp; 71 | s = s + (qq + r); 72 | // } 73 | let d = p + s; 74 | 75 | if (isOverflow(d)) { 76 | throw new VError("Vector2D.dot(): overflowed product"); 77 | } 78 | return d; 79 | } 80 | 81 | public async isEqualTol(w: Vector2D, tol: number): Promise { 82 | if (!isValidTolerance(tol)) { 83 | throw new VError("Vector2D.is_equal_tol(): invalid tolerance"); 84 | } 85 | 86 | let x = Math.abs(this._x - w._x); 87 | let y = Math.abs(this._y - w._y); 88 | 89 | let isEqual = x <= tol && y <= tol; 90 | return isEqual; 91 | } 92 | 93 | // Returns this vector. 94 | public async scale(s: number): Promise { 95 | let newX = this._x * s; 96 | let newY = this._y * s; 97 | if (isOverflow(newX) || isOverflow(newY)) { 98 | throw new VError("Vector2D.scale(): overflowed product"); 99 | } 100 | this._x = newX; 101 | this._y = newY; 102 | return this; 103 | } 104 | 105 | // Returns this vector. 106 | public add(w: this): this { 107 | this._x += w._x; 108 | this._y += w._y; 109 | return this; 110 | } 111 | 112 | // Returns this vector. 113 | public sub(w: this): this { 114 | this._x -= w._x; 115 | this._y -= w._y; 116 | return this; 117 | } 118 | 119 | /// Gets the length of the vector. 120 | public async length(): Promise { 121 | let len = nrm2(this._x, this._y); 122 | return resultFromCalculation(len); 123 | } 124 | 125 | /// Gets the squared length of the vector. 126 | public async lengthSquared(): Promise { 127 | let x = this._x; 128 | let y = this._y; 129 | let len = x * x + y * y; 130 | return resultFromCalculation(len); 131 | } 132 | 133 | // Modifies unit length codirectional to this vector. 134 | // Returns this vector. 135 | public async normalize(): Promise { 136 | let ln; 137 | try { 138 | ln = await this.length(); 139 | } catch(e) { 140 | throw new VError(e as Error, "Vector2D.normalize(): could not get length of self"); 141 | } 142 | if (ln === 0.0 || (this._x === 0.0 && this._y === 0.0)) { 143 | throw new VError("Vector2D.get_normalized_vector: vector to normalize has zero length"); 144 | } 145 | this._x /= ln; 146 | this._y /= ln; 147 | return this; 148 | } 149 | 150 | public async angleBetweenVectors(w: this): Promise { 151 | // formula by Kahan 152 | let u: Vector2D, v: Vector2D; 153 | try { 154 | u = await this.clone().normalize(); 155 | } catch (e) { 156 | throw new VError(e as Error, "Vector2D.angle_between_vectors: unable to get u normalized vector"); 157 | } 158 | try { 159 | v = await w.clone().normalize(); 160 | } catch (e) { 161 | throw new VError(e as Error, "Vector2D.angle_between_vectors: unable to get v normalized vector"); 162 | } 163 | 164 | let y: number, x: number; 165 | try { 166 | y = await u.clone().sub(v).length(); 167 | } catch (e) { 168 | throw new VError(e as Error, "Vector2D.angle_between_vectors: unable to compute u-v"); 169 | } 170 | try { 171 | x = await u.clone().add(v).length(); 172 | } catch (e) { 173 | throw new VError(e as Error, "Vector2D.angle_between_vectors: unable to compute u+v"); 174 | } 175 | return 2.0 * Math.atan2(y, x); 176 | } 177 | 178 | // Computes the cosine of the angle between the two vectors via the dot product. 179 | public async dotCosine(w: this): Promise { 180 | let vv: Vector2D, ww: Vector2D; 181 | try { 182 | vv = await this.clone().normalize(); 183 | } catch (e) { 184 | throw new VError(e as Error, "Vector2D.dot_cosine: unable to get normalized vector from this vector"); 185 | } 186 | try { 187 | ww = await w.clone().normalize(); 188 | } catch (e) { 189 | throw new VError(e as Error, "Vector2D.dot_cosine: unable to get normalized vector from argument vector"); 190 | } 191 | const d = vv._x * ww._x + vv._y * ww._y; 192 | if (isOverflow(d)) { 193 | throw new VError("Vector2D.dot_cosine: unable to compute dot product for cosine"); 194 | } 195 | return d; 196 | } 197 | 198 | /// Computes the angle from this vector to the given vector. 199 | public async angleTo(w: this): Promise { 200 | let lv, lw; 201 | try { 202 | lv = await this.length(); 203 | } catch (e) { 204 | throw new VError(e as Error, "Vector2D.angle_to: cannot get length of self") 205 | } 206 | try { 207 | lw = await w.length(); 208 | } catch (e) { 209 | throw new VError(e as Error, "Vector2D.angle_to: cannot get length of argument") 210 | } 211 | 212 | const u = this.clone().sub(w); 213 | let c; 214 | try { 215 | c = await u.length(); 216 | } catch (e) { 217 | throw new VError(e as Error, "Vector2D.angle_to: cannot get length of vector difference") 218 | } 219 | 220 | let a = Math.max(lv, lw); 221 | let b = Math.min(lv, lw); 222 | 223 | let res = inscribedAngleTriangle(a, b, c); 224 | return resultFromCalculation(res); 225 | } 226 | 227 | /// Computes the canonical angle in the 2D plane. 228 | public async angle(): Promise { 229 | return this.angleTo(Vector2D.XAXIS_2D as this); 230 | } 231 | 232 | /// Gets a vector perpendicular to this vector. 233 | public getPerpendicular(): this { 234 | return new Vector2D(-this._y, this._x) as this; 235 | } 236 | 237 | /// Returns true if the given vector's components are equal to the zero vector within a tolerance. 238 | public async isZeroLength(tol: number): Promise { 239 | return await this.isEqualTol(Vector2D.ZERO_2D, tol); 240 | } 241 | 242 | /// Returns true if the given vector's components are equal to the normalized vector codirectional to this vector within a tolerance. 243 | public async isUnitLength(tol: number): Promise { 244 | const v = await this.clone().normalize(); 245 | return this.isEqualTol(v, tol); 246 | } 247 | 248 | /// Returns true if the given vector is parallel to this vector within a tolerance. 249 | public async isParallelTo(w: this, tol: number): Promise { 250 | if (!isValidTolerance(tol)) { 251 | throw new VError("Vector2D.is_parallel_to(): invalid tolerance"); 252 | } 253 | 254 | let v1: Vector2D, v2: Vector2D; 255 | try { 256 | v1 = await this.clone().normalize(); 257 | } catch (e) { 258 | throw new VError(e as Error, "Vector2D.is_parallel_to: error getting normalized vector"); 259 | } 260 | try { 261 | v2 = await w.clone().normalize(); 262 | } catch (e) { 263 | throw new VError(e as Error, "Vector2D.is_parallel_to: error getting normalized vector"); 264 | } 265 | 266 | const d = await v1.dot(v2); 267 | let t = Math.abs(Math.abs(d) - 1.0); 268 | return t <= tol; 269 | } 270 | 271 | /// Returns true if the given vector is perpendicular to this vector within a tolerance. 272 | public async isPerpendicularTo(w: this, tol: number): Promise { 273 | if (!isValidTolerance(tol)) { 274 | throw new VError("Vector2D.is_perpendicular_to(): invalid tolerance"); 275 | } 276 | 277 | let v1: Vector2D, v2: Vector2D; 278 | try { 279 | v1 = await this.clone().normalize(); 280 | } catch (e) { 281 | throw new VError(e as Error, "Vector2D.is_perpendicular_to: error getting normalized vector"); 282 | } 283 | try { 284 | v2 = await w.clone().normalize(); 285 | } catch (e) { 286 | throw new VError(e as Error, "Vector2D.is_perpendicular_to: error getting normalized vector"); 287 | } 288 | 289 | const d = await v1.dot(v2); 290 | return (d <= tol); 291 | } 292 | 293 | /// Returns true if the given vector is the vector is codirectional to this vector. 294 | public async isCodirectionalTo(w: this, tol: number): Promise { 295 | if (!isValidTolerance(tol)) { 296 | throw new VError("Vector2D.is_codirectional_to(): invalid tolerance"); 297 | } 298 | 299 | let v0: Vector2D, v1: Vector2D; 300 | try { 301 | v0 = await this.clone().normalize(); 302 | } catch (e) { 303 | throw new VError(e as Error, "Vector2D.is_codirectional_to: error getting normalized vector"); 304 | } 305 | try { 306 | v1 = await w.clone().normalize(); 307 | } catch (e) { 308 | throw new VError(e as Error, "Vector2D.is_codirectional_to: error getting normalized vector"); 309 | } 310 | 311 | // test to see if direction is the same 312 | const d = await v0.dot(v1); 313 | if (d < 0.0) { 314 | return false; 315 | } 316 | 317 | // test if lines are parallel 318 | let isParallel: boolean; 319 | try { 320 | isParallel = await v0.isParallelTo(v1, tol); 321 | } catch (e) { 322 | throw new VError(e as Error, "Vector2D.is_codirectional_to: error determining parallelism for vectors"); 323 | } 324 | return isParallel; 325 | } 326 | } 327 | 328 | // #[cfg(test)] 329 | // mod tests { 330 | // use super::*; 331 | // #[test] 332 | // fn vec2d_create() { 333 | // let v = Vector2D{x: 2.0, y: 3.0}; 334 | // assert_eq!(v.x, 2.0); 335 | // assert_eq!(v.y, 3.0); 336 | // } 337 | // #[test] 338 | // fn vec2d_get_components() { 339 | // let v = Vector2D{x: 2.0, y: 3.0}; 340 | // assert_eq!(v.x, 2.0); 341 | // assert_eq!(v.y, 3.0); 342 | // let (x, y) = v.get_components(); 343 | // assert_eq!(x, 2.0); 344 | // assert_eq!(y, 3.0); 345 | // } 346 | // #[test] 347 | // fn vec2d_get_length() { 348 | // let v = Vector2D{x: 3.0, y: 4.0}; 349 | // let h = v.length(); 350 | // assert_eq!(h.is_err(), false); 351 | // assert_eq!(h.unwrap(), 5.0); 352 | // let hsq = v.length_squared(); 353 | // assert_eq!(hsq.is_err(), false); 354 | // assert_eq!(hsq.unwrap(), 25.0); 355 | // } 356 | // #[test] 357 | // fn vec2d_get_angle() { 358 | // let v0 = Vector2D{x: f64::sqrt(3.0) / 2.0, y: 0.5 }; 359 | // let ra0 = v0.angle(); 360 | // assert_eq!(ra0.is_err(), false); 361 | // let a0 = ra0.unwrap(); 362 | // assert_eq!(a0, std::f64::consts::PI / 6.0); 363 | // } 364 | // #[test] 365 | // fn vec2d_dot() { 366 | // let v0 = Vector2D{x: 2.0, y: 3.0}; 367 | // let v1 = Vector2D{x: 5.0, y: 7.0}; 368 | // let d = v0.dot(v1); 369 | // assert_eq!(d.is_err(), false); 370 | // assert_eq!(d.unwrap(), 31.0); 371 | // } 372 | // #[test] 373 | // fn vec2d_perp_vector() { 374 | // let v0 = Vector2D{x: 2.3, y: 3.7}; 375 | // let p = v0.get_perpendicular_vector(); 376 | // let d = v0.dot(p); 377 | // assert_eq!(d.is_err(), false); 378 | // assert_eq!(f64::abs(d.unwrap()) < 1e-14, true); 379 | // } 380 | // #[test] 381 | // fn vec2d_normalized_vector() { 382 | // let v0 = Vector2D{x: 2.3, y: 3.7}; 383 | // let vres = v0.get_normalized_vector(); 384 | // assert_eq!(vres.is_err(), false); 385 | // let res = vres.unwrap().length(); 386 | // assert_eq!(res.is_err(), false); 387 | // assert_eq!((res.unwrap() - 1.0) < 1e-14, true); 388 | // } 389 | // #[test] 390 | // fn vec2d_almost_equal_vectors() { 391 | // let e = 1e-13; 392 | // let v0 = Vector2D{x: 2.3, y: 3.7}; 393 | // let v1 = Vector2D{x: v0.x, y: v0.y - e}; 394 | // let equal_res = v0.is_equal_tol(v1, e); 395 | // assert_eq!(equal_res.is_err(), false); 396 | // assert_eq!(equal_res.unwrap(), true); 397 | // } 398 | // #[test] 399 | // fn vec2d_zero_vector() { 400 | // let e = 1e-13; 401 | // let v0 = Vector2D{x: e, y: 0.0}; 402 | // let zero_res = v0.is_zero_length(e); 403 | // assert_eq!(zero_res.is_err(), false); 404 | // assert_eq!(zero_res.unwrap(), true); 405 | 406 | // let zero_res1 = v0.is_zero_length(1e-14); 407 | // assert_eq!(zero_res1.is_err(), false); 408 | // assert_eq!(zero_res1.unwrap(), false); 409 | // } 410 | // #[test] 411 | // fn vec2d_unit_vector() { 412 | // let e = 1e-13; 413 | // let v0 = Vector2D{x: 1.0 - e, y: 0.0}; 414 | // let unit_res = v0.is_unit_length(2.0 * e); 415 | // assert_eq!(unit_res.is_err(), false); 416 | // assert_eq!(unit_res.unwrap(), true); 417 | // } 418 | // #[test] 419 | // fn vec2d_parallel_vectors() { 420 | // let e = 1e-14; 421 | // let v0 = Vector2D{ x: 1.0, y: 2.0 }; 422 | // let v1 = Vector2D{ x: -1.0 - e, y: -2.0 - e }; 423 | // let unit_res = v0.is_parallel_to(v1, e); 424 | // assert_eq!(unit_res.is_err(), false); 425 | // assert_eq!(unit_res.unwrap(), true); 426 | 427 | // let v1 = Vector2D{ x: -1.0 - 1e-6, y: -2.0 - 1e-6 }; 428 | // let unit_res = v0.is_parallel_to(v1, e); 429 | // assert_eq!(unit_res.is_err(), false); 430 | // assert_eq!(unit_res.unwrap(), false); 431 | // } 432 | // #[test] 433 | // fn vec2d_perp_vectors() { 434 | // let e = 1e-13; 435 | // let v0 = Vector2D{x: 1.0, y: 2.0}; 436 | // let p0 = v0.get_perpendicular_vector(); 437 | 438 | // println!("{}", p0); 439 | 440 | // let v1 = Vector2D{x: p0.x + e, y: p0.y + e}; 441 | // let unit_res = v0.is_perpendicular_to(v1, e); 442 | // assert_eq!(unit_res.is_err(), false); 443 | // assert_eq!(unit_res.unwrap(), true); 444 | 445 | // let v1 = Vector2D{x: p0.x + (2.0 * e), y: p0.y + (2.0 * e)}; 446 | // let unit_res = v0.is_perpendicular_to(v1, e); 447 | // assert_eq!(unit_res.is_err(), false); 448 | // assert_eq!(unit_res.unwrap(), false); 449 | // } 450 | // #[test] 451 | // fn vec2d_codirectional_vectors() { 452 | // let e = 1e-14; 453 | 454 | // let v0 = Vector2D{ x: 1.0, y: 2.0 }; 455 | // let v1 = Vector2D{ x: 1.0 + e, y: 2.0 + e }; 456 | // let res = v0.is_codirectional_to(v1, e); 457 | // assert_eq!(res.is_err(), false); 458 | // assert_eq!(res.unwrap(), true); 459 | 460 | // let v1 = Vector2D{ x: 1.0, y: 2.0 + 1e-6 }; 461 | // let res = v0.is_codirectional_to(v1, e); 462 | // assert_eq!(res.is_err(), false); 463 | // assert_eq!(res.unwrap(), false); 464 | // } 465 | // } -------------------------------------------------------------------------------- /src/math/vector3.ts: -------------------------------------------------------------------------------- 1 | import VError from "verror"; 2 | 3 | import { 4 | twoProduct, 5 | twoSum, 6 | isValidTolerance, 7 | isOverflow, 8 | resultFromCalculation, 9 | inscribedAngleTriangle, 10 | nrm2 11 | } from "./numeric"; 12 | 13 | export class Vector3D { 14 | public static ZERO_3D: Vector3D = new Vector3D(0.0, 0.0, 0.0); 15 | public static XAXIS_3D: Vector3D = new Vector3D(1.0, 0.0, 0.0); 16 | public static YAXIS_3D: Vector3D = new Vector3D(0.0, 1.0, 0.0); 17 | public static ZAXIS_3D: Vector3D = new Vector3D(0.0, 0.0, 1.0); 18 | 19 | private _x: number; 20 | private _y: number; 21 | private _z: number; 22 | 23 | constructor(x: number, y: number, z: number) { 24 | this._x = x; 25 | this._y = y; 26 | this._z = z; 27 | } 28 | 29 | public print(): string { 30 | return `({${this._x}}, {${this._y}}, {${this._z})`; 31 | } 32 | 33 | public clone(): this { 34 | return new Vector3D(this._x, this._y, this._z) as this; 35 | } 36 | 37 | /// Computes the dot product between two vectors. 38 | public async dot(w: this): Promise { 39 | let x1 = this._x; 40 | let x2 = this._y; 41 | let x3 = this._z; 42 | let y1 = w._x; 43 | let y2 = w._y; 44 | let y3 = w._z; 45 | 46 | let [p, s] = twoProduct(x1, y1); 47 | // for i in 2..4 { 48 | let [h1, r1] = twoProduct(x2, y2); 49 | let [pp1, qq1] = twoSum(p, h1); 50 | p = pp1; 51 | s = s + (qq1 + r1); 52 | 53 | let [h2, r2] = twoProduct(x3, y3); 54 | let [pp2, qq2] = twoSum(p, h2); 55 | p = pp2; 56 | s = s + (qq2 + r2); 57 | // } 58 | 59 | let d = p + s; 60 | if (isOverflow(d)) { 61 | throw new VError("Vector3D.dot(): overflowed result"); 62 | } 63 | return d; 64 | } 65 | 66 | /// Returns true if the given vector's components are equal to this vector's components within a tolerance. 67 | public async isEqualTol(w: this, tol: number): Promise { 68 | if (!isValidTolerance(tol)) { 69 | throw new VError("Vector3D.is_equal_tol(): invalid tolerance"); 70 | } 71 | 72 | let x = Math.abs(this._x - w._x); 73 | let y = Math.abs(this._y - w._y); 74 | let z = Math.abs(this._z - w._z); 75 | 76 | let is_equal = x <= tol && y <= tol && z <= tol; 77 | return is_equal; 78 | } 79 | 80 | /// Scales the vector components by the given scalar. 81 | public async scale(s: number): Promise { 82 | const newX = this._x * s; 83 | const newY = this._y * s; 84 | const newZ = this._z * s; 85 | 86 | if (isOverflow(newX) || isOverflow(newY) || isOverflow(newZ)) { 87 | throw new VError("Vector3D.scale(): scale resulted in overflow"); 88 | } 89 | 90 | return new Vector3D(newX, newY, newZ) as this; 91 | } 92 | 93 | public add(w: this): this { 94 | const newX = this._x + w._x; 95 | const newY = this._y + w._y; 96 | const newZ = this._z + w._z; 97 | if (isOverflow(newX) || isOverflow(newY) || isOverflow(newZ)) { 98 | throw new VError("Vector3D.add(): add resulted in overflow"); 99 | } 100 | this._x = newX; 101 | this._y = newY; 102 | this._z = newZ; 103 | return this; 104 | } 105 | 106 | public sub(w: this): this { 107 | const newX = this._x - w._x; 108 | const newY = this._y - w._y; 109 | const newZ = this._z - w._z; 110 | if (isOverflow(newX) || isOverflow(newY) || isOverflow(newZ)) { 111 | throw new VError("Vector3D.sub(): sub resulted in overflow"); 112 | } 113 | this._x = newX; 114 | this._y = newY; 115 | this._z = newZ; 116 | return this; 117 | } 118 | 119 | public get x(): number { 120 | return this._x; 121 | } 122 | 123 | public get y(): number { 124 | return this._y; 125 | } 126 | 127 | public get z(): number { 128 | return this._z; 129 | } 130 | 131 | public setX(x: number): void { 132 | this._x = x; 133 | } 134 | 135 | public setY(y: number): void { 136 | this._y = y; 137 | } 138 | 139 | public setZ(z: number): void { 140 | this._z = z; 141 | } 142 | 143 | public set(x: number, y: number, z: number): void { 144 | this._x = x; 145 | this._y = y; 146 | this._z = z; 147 | } 148 | 149 | public async angleBetweenVectors(w: this): Promise { 150 | let u, v; 151 | try { 152 | u = await this.normalize(); 153 | } catch (e) { 154 | throw new VError("Vector3D.angle_between_vectors(): error normalizing this vector"); 155 | } 156 | try { 157 | v = await w.normalize(); 158 | } catch (e) { 159 | throw new VError("Vector3D.angle_between_vectors(): error normalizing vector argument"); 160 | } 161 | 162 | let x, y; 163 | try { 164 | y = await u.clone().sub(v).length(); 165 | } catch (e) { 166 | throw new VError("Vector3D.angle_between_vectors(): error normalizing vector difference"); 167 | } 168 | try { 169 | x = await u.clone().add(v).length(); 170 | } catch (e) { 171 | throw new VError("Vector3D.angle_between_vectors(): error normalizing vector sum"); 172 | } 173 | return 2.0 * Math.atan2(y, x); 174 | } 175 | 176 | /// Gets the components of the vector. 177 | public getComponents(): [number, number, number] { return [this._x, this._y, this._z]; } 178 | 179 | /// Gets the length of the vector. 180 | public async length(): Promise { 181 | let len = nrm2(nrm2(this._x, this._y), this._z); 182 | return resultFromCalculation(len); 183 | } 184 | 185 | /// Gets the squared length of the vector. 186 | public async lengthSquared(): Promise { 187 | let x = this._x; 188 | let y = this._y; 189 | let z = this._z; 190 | 191 | let len = x * x + y * y + z * z; 192 | return resultFromCalculation(len); 193 | } 194 | 195 | // Computes the cosine of the angle between the two vectors via the dot product. 196 | public async dotCosine(w: this): Promise { 197 | let vv, ww; 198 | try { 199 | vv = await this.normalize(); 200 | } catch (e) { 201 | throw new VError("Vector3D.dot_cosine(): error normalizing this vector"); 202 | } 203 | try { 204 | ww = await w.normalize(); 205 | } catch (e) { 206 | throw new VError("Vector3D.dot_cosine(): error normalizing vector argument"); 207 | } 208 | 209 | let d = vv._x * ww._x + vv._y * ww._y + vv._z * ww._z; 210 | if (isOverflow(d)) { 211 | throw new VError("Vector3D.dot_cosine: error getting dot product"); 212 | } 213 | return d; 214 | } 215 | 216 | /// Computes the angle from this vector to the given vector. 217 | public async angleTo(w: this): Promise { 218 | let lv, lw; 219 | try { 220 | lv = await this.length(); 221 | } catch (e) { 222 | throw new VError(e as Error, "Vector3D.angle_to: error getting length of this vector"); 223 | } 224 | try { 225 | lw = await w.length(); 226 | } catch (e) { 227 | throw new VError(e as Error, "Vector3D.angle_to: error getting length of vector argument"); 228 | } 229 | 230 | let u = this.clone().sub(w); 231 | let c; 232 | try { 233 | c = await u.length(); 234 | } catch (e) { 235 | throw new VError(e as Error, "Vector3D.angle_to: error getting length of vector difference"); 236 | }; 237 | 238 | let a = Math.max(lv, lw); 239 | let b = Math.min(lv, lw); 240 | 241 | let res = inscribedAngleTriangle(a, b, c); 242 | return resultFromCalculation(res); 243 | } 244 | 245 | /// Gets a vector perpendicular to this vector. 246 | public getPerpendicularVector(): this { 247 | let x = this._x; 248 | let y = this._y; 249 | 250 | let tol = 0.015625; 251 | if (Math.abs(x) < tol && Math.abs(y) < tol) { 252 | return new Vector3D(this._z, 0.0, -this._x) as this; 253 | } else { 254 | return new Vector3D(this._y, -this._x, 0.0) as this; 255 | } 256 | } 257 | 258 | /// Calculates the cross product between this vector and a given vector. 259 | public cross(v: this): this { 260 | let a1 = this._x; 261 | let a2 = this._y; 262 | let a3 = this._z; 263 | 264 | let b1 = v._x; 265 | let b2 = v._y; 266 | let b3 = v._z; 267 | 268 | const newX = a2 * b3 - a3 * b2; 269 | const newY = -(a1 * b3 - a3 * b1); 270 | const newZ = a1 * b2 - a2 * b1; 271 | 272 | if (isOverflow(newX) || isOverflow(newY) || isOverflow(newZ)) { 273 | throw new VError("Vector3D.cross(): cross resulted in overflow"); 274 | } 275 | return new Vector3D(newX, newY, newZ) as this; 276 | } 277 | 278 | /// Gets a vector with unit length codirectional to this vector. 279 | public async normalize(): Promise { 280 | let ln; 281 | try { 282 | ln = await this.length(); 283 | } catch(e) { 284 | throw new VError("Vector3D.normalize(): error getting length of this vector"); 285 | } 286 | if (ln === 0.0) { 287 | throw new VError("Vector3D.get_normalized_vector: length of this is zero length"); 288 | } 289 | const newX = this._x / ln; 290 | const newY = this._y / ln; 291 | const newZ = this._z / ln; 292 | if (isOverflow(newX) || isOverflow(newY) || isOverflow(newZ)) { 293 | throw new VError("Vector3D.normalize(): cross resulted in overflow"); 294 | } 295 | return new Vector3D(newX, newY, newZ) as this; 296 | } 297 | 298 | /// Returns true if the given vector's components are equal to the zero vector within a tolerance. 299 | public async isZeroLength(tol: number): Promise { 300 | return this.isEqualTol(Vector3D.ZERO_3D as this, tol) 301 | } 302 | 303 | /// Returns true if the given vector's components are equal to the normalized vector codirectional to this vector within a tolerance. 304 | public async isUnitLength(tol: number): Promise { 305 | let v; 306 | try { 307 | v = await this.clone().normalize(); 308 | } catch (e) { 309 | throw new VError("Vector3D.is_unit_length: error normalizing this vector"); 310 | } 311 | return this.isEqualTol(v, tol); 312 | } 313 | 314 | /// Returns true if the given vector is parallel to this vector within a tolerance. 315 | public async isParallelTo(w: this, tol: number): Promise { 316 | if (!isValidTolerance(tol)) { 317 | throw new VError("Vector3D.is_parallel_to(): invalid tolerance"); 318 | } 319 | 320 | let v1, v2, d; 321 | try { 322 | v1 = await this.clone().normalize(); 323 | } catch (e) { 324 | throw new VError("Vector3D.is_parallel_to: error getting normalized vector"); 325 | } 326 | try { 327 | v2 = await w.clone().normalize(); 328 | } catch (e) { 329 | throw new VError("Vector3D.is_parallel_to: error getting normalized vector"); 330 | } 331 | try { 332 | d = await v1.dot(v2); 333 | } catch (e) { 334 | throw new VError("Vector3D.is_parallel_to: error getting dot product between vectors"); 335 | } 336 | 337 | let t = Math.abs(d) - 1.0; 338 | return Math.abs(t) <= tol; 339 | } 340 | 341 | /// Returns true if the given vector is perpendicular to this vector within a tolerance. 342 | public async isPerpendicularTo(w: this, tol: number): Promise { 343 | if (!isValidTolerance(tol)) { 344 | throw new VError("Vector3D.is_perpendicular_to(): invalid tolerance"); 345 | } 346 | 347 | let v1, v2, d; 348 | try { 349 | v1 = await this.clone().normalize(); 350 | } catch (e) { 351 | throw new VError("Vector3D.is_perpendicular_to: error getting normalized vector"); 352 | } 353 | try { 354 | v2 = await w.clone().normalize(); 355 | } catch (e) { 356 | throw new VError("Vector3D.is_perpendicular_to: error getting normalized vector"); 357 | } 358 | try { 359 | d = await v1.dot(v2); 360 | } catch (e) { 361 | throw new VError("Vector3D.is_perpendicular_to: error getting dot product between vectors"); 362 | } 363 | 364 | return d <= tol; 365 | } 366 | 367 | /// Returns true if the given vector is the vector is codirectional to this vector. 368 | public async isCodirectionalTo(w: this, tol: number): Promise { 369 | if (!isValidTolerance(tol)) { 370 | throw new VError("Vector3D.is_codirectional_to(): invalid tolerance"); 371 | } 372 | 373 | let d; 374 | try { 375 | d = await this.dot(w); 376 | } catch (e) { 377 | throw new VError(e as Error, "Vector3D.is_codirectional_to: error getting dot product for vector codirectionality"); 378 | } 379 | 380 | if (d < 0.0) { 381 | return false; 382 | } 383 | 384 | let is_parallel; 385 | try { 386 | is_parallel = await this.isParallelTo(w, tol) 387 | } catch(e) { 388 | throw new VError("Vector3D.is_codirectional_to: error determining parallelism between vectors"); 389 | }; 390 | 391 | return is_parallel; 392 | } 393 | } 394 | 395 | // #[cfg(test)] 396 | // mod tests { 397 | // use super::*; 398 | // #[test] 399 | // fn vec3d_create() { 400 | // let v = Vector3D{x: 2.0, y: 3.0, z: 5.0}; 401 | // assert_eq!(v.x, 2.0); 402 | // assert_eq!(v.y, 3.0); 403 | // assert_eq!(v.z, 5.0); 404 | // } 405 | // #[test] 406 | // fn vec3d_get_components() { 407 | // let v = Vector3D{x: 2.0, y: 3.0, z: 5.0}; 408 | // assert_eq!(v.x, 2.0); 409 | // assert_eq!(v.y, 3.0); 410 | // assert_eq!(v.z, 5.0); 411 | // let (x, y, z) = v.get_components(); 412 | // assert_eq!(x, 2.0); 413 | // assert_eq!(y, 3.0); 414 | // assert_eq!(z, 5.0); 415 | // } 416 | // #[test] 417 | // fn vec3d_get_length() { 418 | // let v = Vector3D{x: 3.0, y: 4.0, z: 5.0}; 419 | // let h = v.length(); 420 | // assert_eq!(h.is_err(), false); 421 | // assert_eq!(h.unwrap(), number::sqrt(50.0)); 422 | // let hsq = v.length_squared(); 423 | // assert_eq!(hsq.is_err(), false); 424 | // assert_eq!(hsq.unwrap(), 50.0); 425 | // } 426 | // #[test] 427 | // fn vec3d_get_angle() { 428 | // let v0 = Vector3D{x: 3.0, y: -4.0, z: 5.0 }; 429 | // let v1 = Vector3D{x: 2.0, y: 7.0, z: -3.0 }; 430 | // let ra0 = v0.angle_to(v1); 431 | // assert_eq!(ra0.is_err(), false); 432 | 433 | // let wolfram_ans = 2.297673876007866989; 434 | // assert_eq!((ra0.unwrap() - wolfram_ans) < 1e-14, true); 435 | // } 436 | // #[test] 437 | // fn vec3d_get_perp_vector() { 438 | // let v0 = Vector3D{x: 3.0, y: -4.0, z: 5.0 }; 439 | // let v1 = v0.get_perpendicular_vector(); 440 | // let d = v0.dot(v1); 441 | // assert_eq!(d.is_err(), false); 442 | // assert_eq!(d.unwrap() < 1e-14, true); 443 | // } 444 | // #[test] 445 | // fn vec3d_get_norm_vector() { 446 | // let v0 = Vector3D{x: 3.0, y: -4.0, z: 5.0 }; 447 | // let v1 = v0.get_normalized_vector(); 448 | // assert_eq!(v1.is_err(), false); 449 | // let v1 = v1.unwrap(); 450 | // let len = v1.length(); 451 | // assert_eq!(len.is_err(), false); 452 | // let len = len.unwrap(); 453 | // assert_eq!((len - 1.0) < 1e-14, true); 454 | 455 | // let is_codir = v0.is_codirectional_to(v1, 1e-14); 456 | // assert_eq!(is_codir.is_err(), false); 457 | // assert_eq!(is_codir.unwrap(), true); 458 | // } 459 | // #[test] 460 | // fn vec3d_is_equal_to() { 461 | // let v0 = Vector3D{x: 3.0, y: -4.0, z: 5.0}; 462 | // let e = 1e-14; 463 | // let v1 = Vector3D{x: 3.0 + e, y: -4.0 + e, z: 5.0 + e}; 464 | 465 | // let res1 = v0.is_equal_tol(v1, e * 2.0); 466 | // assert_eq!(res1.is_err(), false); 467 | // assert_eq!(res1.unwrap(), true); 468 | 469 | // let res2 = v0.is_equal_tol(v1, e); 470 | // assert_eq!(res2.is_err(), false); 471 | // assert_eq!(res2.unwrap(), false); 472 | 473 | // let z = Vector3D{x: e, y: e, z: e}; 474 | // let res3 = z.is_zero_length(e); 475 | // assert_eq!(res3.is_err(), false); 476 | // assert_eq!(res3.unwrap(), true); 477 | // } 478 | // } -------------------------------------------------------------------------------- /src/math/matrix3.ts: -------------------------------------------------------------------------------- 1 | import VError from "verror"; 2 | 3 | import { Vector3D } from "./vector3"; 4 | import { 5 | isOverflow, 6 | isValidTolerance, 7 | } from "./numeric"; 8 | 9 | /// A 3x3 matrix. 10 | /// 11 | /// Elements are in row-major format. 12 | export class Matrix3D { 13 | private elements: number[]; 14 | 15 | public constructor(elements: number[]) { 16 | this.elements = elements.slice(0, 9); 17 | } 18 | 19 | public get a00(): number { 20 | return this.elements[0]; 21 | } 22 | 23 | public get a01(): number { 24 | return this.elements[1]; 25 | } 26 | 27 | public get a02(): number { 28 | return this.elements[2]; 29 | } 30 | 31 | public get a10(): number { 32 | return this.elements[3]; 33 | } 34 | 35 | public get a11(): number { 36 | return this.elements[4]; 37 | } 38 | 39 | public get a12(): number { 40 | return this.elements[5]; 41 | } 42 | 43 | public get a20(): number { 44 | return this.elements[6]; 45 | } 46 | 47 | public get a21(): number { 48 | return this.elements[7]; 49 | } 50 | 51 | public get a22(): number { 52 | return this.elements[8]; 53 | } 54 | 55 | public clone(): this { 56 | return new Matrix3D(this.elements) as this; 57 | } 58 | 59 | public print(): string { 60 | const a00 = this.elements[0]; 61 | const a01 = this.elements[1]; 62 | const a02 = this.elements[2]; 63 | const a10 = this.elements[3]; 64 | const a11 = this.elements[4]; 65 | const a12 = this.elements[5]; 66 | const a20 = this.elements[6]; 67 | const a21 = this.elements[7]; 68 | const a22 = this.elements[8]; 69 | 70 | return `([{${a00}}}, {${a01}}, {${a02}}], [{${a10}}}, {${a11}}, {${a12}}], [{${a20}}}, {${a21}}, {${a22}}])`; 71 | } 72 | 73 | public add(w: this): this { 74 | const a00 = this.elements[0] + w.elements[0]; 75 | const a01 = this.elements[1] + w.elements[1]; 76 | const a02 = this.elements[2] + w.elements[2]; 77 | const a10 = this.elements[3] + w.elements[3]; 78 | const a11 = this.elements[4] + w.elements[4]; 79 | const a12 = this.elements[5] + w.elements[5]; 80 | const a20 = this.elements[6] + w.elements[6]; 81 | const a21 = this.elements[7] + w.elements[7]; 82 | const a22 = this.elements[8] + w.elements[8]; 83 | this.elements[0] = a00; 84 | this.elements[1] = a01; 85 | this.elements[2] = a02; 86 | this.elements[3] = a10; 87 | this.elements[4] = a11; 88 | this.elements[5] = a12; 89 | this.elements[6] = a20; 90 | this.elements[7] = a21; 91 | this.elements[8] = a22; 92 | return this; 93 | } 94 | 95 | public sub(w: this): this { 96 | const a00 = this.elements[0] - w.elements[0]; 97 | const a01 = this.elements[1] - w.elements[1]; 98 | const a02 = this.elements[2] - w.elements[2]; 99 | const a10 = this.elements[3] - w.elements[3]; 100 | const a11 = this.elements[4] - w.elements[4]; 101 | const a12 = this.elements[5] - w.elements[5]; 102 | const a20 = this.elements[6] - w.elements[6]; 103 | const a21 = this.elements[7] - w.elements[7]; 104 | const a22 = this.elements[8] - w.elements[8]; 105 | this.elements[0] = a00; 106 | this.elements[1] = a01; 107 | this.elements[2] = a02; 108 | this.elements[3] = a10; 109 | this.elements[4] = a11; 110 | this.elements[5] = a12; 111 | this.elements[6] = a20; 112 | this.elements[7] = a21; 113 | this.elements[8] = a22; 114 | return this; 115 | } 116 | 117 | private mul(v: this, w: this, output: this): this { 118 | const [a, b, c, d, e, f, g, h, i] = v.elements; 119 | const [j, k, l, m, n, o, p, q, r] = w.elements; 120 | const a00 = a * j + b * m + c * p; 121 | const a01 = a * k + b * n + c * q; 122 | const a02 = a * l + b * o + c * r; 123 | const a10 = d * j + e * m + f * p; 124 | const a11 = d * k + e * n + f * q; 125 | const a12 = d * l + e * o + f * r; 126 | const a20 = g * j + h * m + i * p; 127 | const a21 = g * k + h * n + i * q; 128 | const a22 = g * l + h * o + i * r; 129 | output.elements[0] = a00; 130 | output.elements[1] = a01; 131 | output.elements[2] = a02; 132 | output.elements[3] = a10; 133 | output.elements[4] = a11; 134 | output.elements[5] = a12; 135 | output.elements[6] = a20; 136 | output.elements[7] = a21; 137 | output.elements[8] = a22; 138 | return output; 139 | } 140 | 141 | // Multiplies this matrix on the left by the given matrix and returns the result (result = w * this). 142 | public lmul(w: this): this { 143 | return this.mul(w, this, this); 144 | } 145 | 146 | // Multiplies this matrix on the right by the given matrix and returns the result (result = this * w). 147 | public rmul(w: this): this { 148 | return this.mul(this, w, this); 149 | } 150 | 151 | public getElements(): number[] { 152 | return this.elements.slice(0, 9); 153 | } 154 | 155 | public toIdentity(): this { 156 | this.elements = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]; 157 | return this; 158 | } 159 | 160 | public transpose(): this { 161 | let tmp = this.elements[1]; // 1 -> 3 162 | this.elements[1] = this.elements[3]; 163 | this.elements[3] = tmp; 164 | 165 | tmp = this.elements[2]; // 2 -> 6 166 | this.elements[2] = this.elements[6]; 167 | this.elements[6] = tmp; 168 | 169 | tmp = this.elements[5]; // 5 -> 7 170 | this.elements[5] = this.elements[7]; 171 | this.elements[7] = tmp; 172 | 173 | return this; 174 | } 175 | 176 | public async getRow(row: number): Promise { 177 | switch (row) { 178 | case 0: 179 | return new Vector3D(this.elements[0], this.elements[1], this.elements[2] ); 180 | case 1: 181 | return new Vector3D(this.elements[3], this.elements[4], this.elements[5] ); 182 | case 2: 183 | return new Vector3D(this.elements[6], this.elements[7], this.elements[8] ); 184 | default: 185 | throw new VError("Matrix3D.get_row(): invalid index"); 186 | } 187 | } 188 | 189 | public async getCol(col: number): Promise { 190 | switch (col) { 191 | case 0: 192 | return new Vector3D(this.elements[0], this.elements[3], this.elements[6] ); 193 | case 1: 194 | return new Vector3D(this.elements[1], this.elements[4], this.elements[7] ); 195 | case 2: 196 | return new Vector3D(this.elements[2], this.elements[5], this.elements[8] ); 197 | default: 198 | throw new VError("Matrix3D.get_row(): invalid index"); 199 | } 200 | } 201 | 202 | public adjugate(): this { 203 | const [a, b, c, d, e, f, g, h, i] = this.elements; 204 | const b0 = e * i - f * h; 205 | const b1 = -(b * i - c * h); 206 | const b2 = b * f - c * e; 207 | const b3 = -(d * i - f * g); 208 | const b4 = a * i - c * g; 209 | const b5 = -(a * f - c * d); 210 | const b6 = d * h - e * g; 211 | const b7 = -(a * h - b * g); 212 | const b8 = a * e - b * d; 213 | this.elements[0] = b0; 214 | this.elements[1] = b1; 215 | this.elements[2] = b2; 216 | this.elements[3] = b3; 217 | this.elements[4] = b4; 218 | this.elements[5] = b5; 219 | this.elements[6] = b6; 220 | this.elements[7] = b7; 221 | this.elements[8] = b8; 222 | return this; 223 | } 224 | 225 | public async invert(): Promise { 226 | const det = this.determinant(); 227 | if (det === 0.0) { 228 | throw new VError("Matrix3D.get_inverse(): determinant is zero"); 229 | } 230 | return this.adjugate().scale(1.0 / det); 231 | } 232 | 233 | public determinant(): number { 234 | const [a, b, c, d, e, f, g, h, i] = this.elements; 235 | return a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g) 236 | } 237 | 238 | public async isSingular(tol: number): Promise { 239 | if (!isValidTolerance(tol)) { 240 | throw new VError("Matrix3D.is_singular(): invalid tolerance"); 241 | } 242 | const det = this.determinant(); 243 | return (Math.abs(det) < tol); 244 | } 245 | 246 | public async scale(s: number): Promise { 247 | if (isNaN(s)) { 248 | throw new VError("Matrix3D.scale(): scale factor is NaN"); 249 | } 250 | 251 | const e0 = this.elements[0] * s; 252 | const e1 = this.elements[1] * s; 253 | const e2 = this.elements[2] * s; 254 | const e3 = this.elements[3] * s; 255 | const e4 = this.elements[4] * s; 256 | const e5 = this.elements[5] * s; 257 | const e6 = this.elements[6] * s; 258 | const e7 = this.elements[7] * s; 259 | const e8 = this.elements[8] * s; 260 | if (isOverflow(e0) || isOverflow(e1) || isOverflow(e2) || 261 | isOverflow(e3) || isOverflow(e4) || isOverflow(e5) || 262 | isOverflow(e6) || isOverflow(e7) || isOverflow(e8)) { 263 | throw new VError("Matrix3D.scale(): overflowed result"); 264 | } 265 | this.elements[0] = e0; 266 | this.elements[1] = e1; 267 | this.elements[2] = e2; 268 | this.elements[3] = e3; 269 | this.elements[4] = e4; 270 | this.elements[5] = e5; 271 | this.elements[6] = e6; 272 | this.elements[7] = e7; 273 | this.elements[8] = e8; 274 | return this; 275 | } 276 | 277 | public async applyToVector(v: Vector3D): Promise { 278 | let a = this.elements[0]; 279 | let b = this.elements[1]; 280 | let c = this.elements[2]; 281 | const newX = a * v.x + b * v.y + c * v.z; 282 | a = this.elements[3]; 283 | b = this.elements[4]; 284 | c = this.elements[5]; 285 | const newY = a * v.x + b * v.y + c * v.z; 286 | a = this.elements[6]; 287 | b = this.elements[7]; 288 | c = this.elements[8]; 289 | const newZ = a * v.x + b * v.y + c * v.z; 290 | 291 | if (isOverflow(newX) || isOverflow(newY) || isOverflow(newZ)) { 292 | throw new VError("Matrix3D.apply_to_vector(): overflowed result"); 293 | } 294 | v.set(newX, newY, newZ); 295 | return v; 296 | } 297 | } 298 | 299 | // #[cfg(test)] 300 | // mod tests { 301 | // use super::*; 302 | // #[test] 303 | // fn matrix3d_create() { 304 | // let elems: [number; 9] = [2.0, 3.0, 5.0, 7.0, 11.0, 13.0, 17.0, 19.0, 23.0]; 305 | // let v = Matrix3D{ elements: elems }; 306 | // assert_eq!(v.elements[0], 2.0); 307 | // assert_eq!(v.elements[1], 3.0); 308 | // assert_eq!(v.elements[2], 5.0); 309 | // assert_eq!(v.elements[3], 7.0); 310 | // assert_eq!(v.elements[4], 11.0); 311 | // assert_eq!(v.elements[5], 13.0); 312 | // assert_eq!(v.elements[6], 17.0); 313 | // assert_eq!(v.elements[7], 19.0); 314 | // assert_eq!(v.elements[8], 23.0); 315 | // } 316 | 317 | // #[test] 318 | // fn matrix3d_identity() { 319 | // let v = Matrix3D::identity(); 320 | // assert_eq!(v.elements[0], 1.0); 321 | // assert_eq!(v.elements[1], 0.0); 322 | // assert_eq!(v.elements[2], 0.0); 323 | // assert_eq!(v.elements[3], 0.0); 324 | // assert_eq!(v.elements[4], 1.0); 325 | // assert_eq!(v.elements[5], 0.0); 326 | // assert_eq!(v.elements[6], 0.0); 327 | // assert_eq!(v.elements[7], 0.0); 328 | // assert_eq!(v.elements[8], 1.0); 329 | // } 330 | 331 | // #[test] 332 | // fn matrix3d_transpose() { 333 | // let elems: [number; 9] = [2.0, 3.0, 5.0, 7.0, 11.0, 13.0, 17.0, 19.0, 23.0]; 334 | // let v = Matrix3D{ elements: elems }; 335 | // let vt = v.transpose(); 336 | 337 | // assert_eq!(v.elements[0], vt.elements[0]); 338 | // assert_eq!(v.elements[1], vt.elements[3]); 339 | // assert_eq!(v.elements[2], vt.elements[6]); 340 | // assert_eq!(v.elements[3], vt.elements[1]); 341 | // assert_eq!(v.elements[4], vt.elements[4]); 342 | // assert_eq!(v.elements[5], vt.elements[7]); 343 | // assert_eq!(v.elements[6], vt.elements[2]); 344 | // assert_eq!(v.elements[7], vt.elements[5]); 345 | // assert_eq!(v.elements[8], vt.elements[8]); 346 | // } 347 | 348 | // #[test] 349 | // fn matrix3d_get_row() { 350 | // let elems: [number; 9] = [2.0, 3.0, 5.0, 7.0, 11.0, 13.0, 17.0, 19.0, 23.0]; 351 | // let v = Matrix3D{ elements: elems }; 352 | 353 | // let r = v.get_row(0); 354 | // assert!(r.is_ok()); 355 | // let r = r.unwrap(); 356 | // assert_eq!(r.x(), v.elements[0]); 357 | // assert_eq!(r.y(), v.elements[1]); 358 | // assert_eq!(r.z(), v.elements[2]); 359 | 360 | // let r = v.get_row(1); 361 | // assert!(r.is_ok()); 362 | // let r = r.unwrap(); 363 | // assert_eq!(r.x(), v.elements[3]); 364 | // assert_eq!(r.y(), v.elements[4]); 365 | // assert_eq!(r.z(), v.elements[5]); 366 | 367 | // let r = v.get_row(2); 368 | // assert!(r.is_ok()); 369 | // let r = r.unwrap(); 370 | // assert_eq!(r.x(), v.elements[6]); 371 | // assert_eq!(r.y(), v.elements[7]); 372 | // assert_eq!(r.z(), v.elements[8]); 373 | 374 | // let r = v.get_row(3); 375 | // assert!(r.is_err()); 376 | // } 377 | 378 | // #[test] 379 | // fn matrix3d_get_col() { 380 | // let elems: [number; 9] = [2.0, 3.0, 5.0, 7.0, 11.0, 13.0, 17.0, 19.0, 23.0]; 381 | // let v = Matrix3D{ elements: elems }; 382 | 383 | // let r = v.get_col(0); 384 | // assert!(r.is_ok()); 385 | // let r = r.unwrap(); 386 | // assert_eq!(r.x(), v.elements[0]); 387 | // assert_eq!(r.y(), v.elements[3]); 388 | // assert_eq!(r.z(), v.elements[6]); 389 | 390 | // let r = v.get_col(1); 391 | // assert!(r.is_ok()); 392 | // let r = r.unwrap(); 393 | // assert_eq!(r.x(), v.elements[1]); 394 | // assert_eq!(r.y(), v.elements[4]); 395 | // assert_eq!(r.z(), v.elements[7]); 396 | 397 | // let r = v.get_col(2); 398 | // assert!(r.is_ok()); 399 | // let r = r.unwrap(); 400 | // assert_eq!(r.x(), v.elements[2]); 401 | // assert_eq!(r.y(), v.elements[5]); 402 | // assert_eq!(r.z(), v.elements[8]); 403 | 404 | // let r = v.get_col(3); 405 | // assert!(r.is_err()); 406 | // } 407 | 408 | // #[test] 409 | // fn matrix3d_singular() { 410 | // let elems: [number; 9] = [2.0, 3.0, 5.0, 7.0, 11.0, 13.0, 14.0, 22.0, 26.0]; 411 | // let v_singular = Matrix3D{ elements: elems }; 412 | // let res = v_singular.is_singular(1e-14); 413 | // assert!(res.is_ok()); 414 | // assert_eq!(res.unwrap(), true); 415 | 416 | // let elems: [number; 9] = [2.0, 3.0, 5.0, 7.0, 11.0, 13.0, 17.0, 19.0, 23.0]; 417 | // let v = Matrix3D{ elements: elems }; 418 | // let res = v.is_singular(1e-14); 419 | // assert!(res.is_ok()); 420 | // assert_eq!(res.unwrap(), false); 421 | // } 422 | 423 | // #[test] 424 | // fn matrix3d_scale() { 425 | // let elems: [number; 9] = [2.0, 3.0, 5.0, 7.0, 11.0, 13.0, 17.0, 19.0, 23.0]; 426 | // let v = Matrix3D{ elements: elems }; 427 | // let s = 4.0; 428 | // let res = v.scale(s); 429 | // assert!(res.is_ok()); 430 | // let m = res.unwrap(); 431 | // assert_eq!(m.elements[0], v.elements[0] * s); 432 | // assert_eq!(m.elements[1], v.elements[1] * s); 433 | // assert_eq!(m.elements[2], v.elements[2] * s); 434 | // assert_eq!(m.elements[3], v.elements[3] * s); 435 | // assert_eq!(m.elements[4], v.elements[4] * s); 436 | // assert_eq!(m.elements[5], v.elements[5] * s); 437 | // assert_eq!(m.elements[6], v.elements[6] * s); 438 | // assert_eq!(m.elements[7], v.elements[7] * s); 439 | // assert_eq!(m.elements[8], v.elements[8] * s); 440 | // } 441 | 442 | // #[test] 443 | // fn matrix3d_apply_to_vector() { 444 | // let elems: [number; 9] = [2.0, 3.0, 5.0, 7.0, 11.0, 13.0, 17.0, 19.0, 23.0]; 445 | // let m = Matrix3D{ elements: elems }; 446 | // let v = Vector3D::new(27.0, 29.0, 31.0 ); 447 | 448 | // let res = m.apply_to_vector(v); 449 | // assert!(res.is_ok()); 450 | // let w = res.unwrap(); 451 | // assert_eq!(w.x(), 2.0 * 27.0 + 3.0 * 29.0 + 5.0 * 31.0); 452 | // assert_eq!(w.y(), 7.0 * 27.0 + 11.0 * 29.0 + 13.0 * 31.0); 453 | // assert_eq!(w.z(), 17.0 * 27.0 + 19.0 * 29.0 + 23.0 * 31.0); 454 | // } 455 | // } --------------------------------------------------------------------------------