├── .github └── workflows │ ├── CI.yml │ └── Release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── foreman.toml ├── requirements.txt ├── roblox-testez.toml ├── roblox.toml ├── scripts ├── get_wally_version_string.py └── upload_model.py ├── selene.toml ├── spec.server.lua ├── src ├── linalg.lua └── linalg.spec.lua ├── standalone-model.project.json ├── unit-tests.project.json ├── wally.lock └── wally.toml /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, workflow_dispatch] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: install selene 10 | run: | 11 | curl https://github.com/Kampfkarren/selene/releases/download/0.9.1/selene-linux -L -o selene 12 | chmod +x ./selene 13 | - name: run selene 14 | run: ./selene ./src 15 | unit-tests: 16 | runs-on: windows-latest 17 | steps: 18 | - uses: actions/checkout@v1 19 | - name: download roblox install script 20 | run: Invoke-WebRequest -Uri "https://raw.githubusercontent.com/OrbitalOwen/roblox-win-installer/master/install.py" -OutFile install.py 21 | - name: download settings file 22 | run: Invoke-WebRequest -Uri "https://raw.githubusercontent.com/OrbitalOwen/roblox-win-installer/master/GlobalSettings_13.xml" -OutFile GlobalSettings_13.xml 23 | - name: install pip deps 24 | run: pip install wget psutil 25 | - name: install roblox 26 | run: python install.py "${{ secrets.ROBLOSECURITY }}" 27 | - name: install foreman 28 | uses: rojo-rbx/setup-foreman@v1 29 | with: 30 | token: ${{ secrets.GITHUB_TOKEN }} 31 | - name: install foreman packages (rojo, run-in-roblox) 32 | run: foreman install 33 | - name: install wally packages 34 | run: wally install 35 | - name: run rojo build 36 | run: rojo build -o .\\unit_tests.rbxlx .\\unit-tests.project.json 37 | - name: run tests 38 | run: run-in-roblox --place .\\unit_tests.rbxlx --script .\\spec.server.lua 39 | -------------------------------------------------------------------------------- /.github/workflows/Release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: [workflow_dispatch] 4 | 5 | jobs: 6 | release: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - name: install python dependencies 11 | run: pip3 install -r requirements.txt 12 | - name: get wally version 13 | id: get_wally_version 14 | run: echo "::set-output name=version-string::$(python3 ./scripts/get_wally_version_string.py)" 15 | - name: install foreman 16 | uses: rojo-rbx/setup-foreman@v1 17 | with: 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | - name: install foreman packages (rojo, run-in-roblox) 20 | run: foreman install 21 | - name: install wally packages 22 | run: wally install 23 | - name: run rojo build 24 | run: rojo build -o ./linalg-${{ steps.get_wally_version.outputs.version-string }}.rbxmx ./standalone-model.project.json 25 | - name: create-release 26 | uses: actions/create-release@latest 27 | id: create_release 28 | with: 29 | draft: false 30 | prerelease: false 31 | release_name: ${{ steps.get_wally_version.outputs.version-string }} 32 | tag_name: ${{ steps.get_wally_version.outputs.version-string }} 33 | body_path: CHANGELOG.md 34 | env: 35 | GITHUB_TOKEN: ${{ github.token }} 36 | - name: upload rbxmx file to release 37 | uses: actions/upload-release-asset@v1 38 | env: 39 | GITHUB_TOKEN: ${{ github.token }} 40 | with: 41 | upload_url: ${{ steps.create_release.outputs.upload_url }} 42 | asset_path: ./linalg-${{ steps.get_wally_version.outputs.version-string }}.rbxmx 43 | asset_name: linalg-${{ steps.get_wally_version.outputs.version-string }}.rbxmx 44 | asset_content_type: form 45 | - name: upload rbxmx file to Roblox 46 | run: | 47 | cd ./scripts 48 | python upload_model.py -a ${{ secrets.ASSET_ID }} -f ../linalg-${{ steps.get_wally_version.outputs.version-string }}.rbxmx -r "${{ secrets.UPLOADER_BOT_ROBLOSECURITY }}" 49 | cd .. 50 | - name: prepare wally package contents 51 | run: | 52 | mkdir -p ~/temp/linalg 53 | cp -r ./{Packages,src} ~/temp/linalg/ 54 | cp ./{CHANGELOG.md,LICENSE,README.md,wally.lock,wally.toml} ~/temp/linalg/ 55 | cp ./standalone-model.project.json ~/temp/linalg/default.project.json 56 | - name: prepare wally auth 57 | run: | 58 | mkdir ~/.wally 59 | echo -e '${{ secrets.WALLY_AUTH }}' > ~/.wally/auth.toml 60 | - name: publish wally package 61 | run: wally publish --project-path ~/temp/linalg/ 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | quickTesting.lua 4 | luacov.stats.out 5 | Packages -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Releases! 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Noah Crowley 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lua Linear Algebra 2 |

3 | 4 | CI status 5 | 6 | 7 | PRs Welcome 8 | 9 | 10 | License: MIT 11 | 12 | 13 | Discord server 14 | 15 |

16 | 17 | A simple script to implement linear algebra functions not provided by the Lua standard API, developed especially for use on Roblox 18 | 19 | ## Installation 20 | ### Wally 21 | [Wally](https://github.com/UpliftGames/wally/) users can install this package by adding the following line to their `Wally.toml` under `[dependencies]`: 22 | ``` 23 | linalg = "bytebit/linalg@1.0.0" 24 | ``` 25 | 26 | Then just run `wally install`. 27 | 28 | ### From model file 29 | Model files are uploaded to every release as `.rbxmx` files. You can download the file from the [Releases page](https://github.com/Bytebit-Org/lua-linalg/releases) and load it into your project however you see fit. 30 | 31 | ### From model asset 32 | New versions of the asset are uploaded with every release. The asset can be added to your Roblox Inventory and then inserted into your Place via Toolbox by getting it [here.](https://www.roblox.com/library/7881451885/linalg-Package) 33 | 34 | ## Documentation 35 | --- 36 | 37 | ### Matrix Functions 38 | #### Matrix Instantiation Functions 39 | 40 |
41 | linalg.matrix.new = function(rows) 42 | 43 | Creates a new matrix 44 | 45 | **Parameters:** 46 | - `rows` (`array>`) 47 | A (m x n) array of numbers to fill the matrix with 48 | 49 | **Returns:** 50 | [t:(m x n) matrix] The new matrix 51 | 52 |
53 | 54 |
55 | linalg.matrix.identity = function(n) 56 | 57 | Creates an identity matrix of size (n x n) 58 | 59 | **Parameters:** 60 | - `n` (`number`) 61 | The size of the matrix 62 | 63 | **Returns:** 64 | [t:(n x n) matrix] The identity matrix 65 | 66 |
67 | 68 |
69 | linalg.matrix.zeros = function(m, n) 70 | 71 | Creates a matrix of all zeros of size (m x n) 72 | 73 | **Parameters:** 74 | - `m` (`number`) 75 | - `n` (`number`) 76 | 77 | **Returns:** 78 | [t:(m x n) matrix] The zeros matrix 79 | 80 |
81 | 82 |
83 | linalg.matrix.zerosLike = function(mat) 84 | 85 | Creates a matrix of all zeros of the same size as the provided matrix 86 | 87 | **Parameters:** 88 | - `[t:(m` 89 | x n) matrix] mat The matrix to copy the size of 90 | 91 | **Returns:** 92 | [t:(m x n) matrix] The zeros matrix 93 | 94 |
95 | 96 |
97 | linalg.matrix.diagonal = function(values) 98 | 99 | Creates a diagonal matrix with the values provided as the diagonal entries 100 | 101 | **Parameters:** 102 | - `values` (`array`) 103 | An n-length array whose entries will be set as the diagonal entries 104 | 105 | **Returns:** 106 | [t:(n x n) matrix] The resulting diagonal matrix 107 | 108 |
109 | 110 | #### Matrix Classification Functions 111 | 112 |
113 | linalg.matrix.isDiagonal = function(mat) 114 | 115 | Determines whether a matrix is diagonal 116 | Does not exclusively refer to square matrices 117 | Refers strictly to whether all non-zero values are on the main diagonal (i.e., a_{ij} = 0 for all i, j where i ~= j) 118 | 119 | **Parameters:** 120 | - `[t:(m` 121 | x n) matrix] mat The matrix to check 122 | 123 | **Returns:** 124 | `boolean` 125 | True if the matrix is diagonal, false otherwise 126 | 127 |
128 | 129 |
130 | linalg.matrix.isUpperTriangular = function(mat) 131 | 132 | Determines whether a matrix is upper triangular 133 | Note that any non-square matrix will return false 134 | 135 | **Parameters:** 136 | - `[t:(m` 137 | x n) matrix] mat The matrix to check 138 | 139 | **Returns:** 140 | `boolean` 141 | True if the matrix is upper triangular, false otherwise 142 | 143 |
144 | 145 |
146 | linalg.matrix.isLowerTriangular = function(mat) 147 | 148 | Determines whether a matrix is lower triangular 149 | Note that any non-square matrix will return false 150 | 151 | **Parameters:** 152 | - `[t:(m` 153 | x n) matrix] mat The matrix to check 154 | 155 | **Returns:** 156 | `boolean` 157 | True if the matrix is lower triangular, false otherwise 158 | 159 |
160 | 161 | #### Basic Matrix Operation Functions 162 | 163 |
164 | linalg.matrix.transpose = function(mat) 165 | 166 | Creates a new matrix that is the transpose of the provided matrix 167 | 168 | **Parameters:** 169 | - `[t:` 170 | (m x n) matrix] mat The matrix to create the transpose of 171 | 172 | **Returns:** 173 | [t: (n x m) matrix] The transpose of mat 174 | 175 |
176 | 177 |
178 | linalg.matrix.expm = function(mat, numIterations) 179 | 180 | Solves for e^mat 181 | Defined as: e^A = \sum_{k=0}^{\infinity} \frac{1}{k!} A^k 182 | Implemented in a naive way to approximate by using iterations 183 | Runtime of O(n^3) 184 | 185 | **Parameters:** 186 | - `[t:(n` 187 | x n) matrix] mat The matrix to use as the exponent 188 | - `numIterations` (`number`) 189 | The number of iterations to take the sum of the taylor series to 190 | 191 | **Returns:** 192 | The matrix exponential approximation 193 | 194 |
195 | 196 | ### Vector Functions 197 | #### Vector Instantiation Functions 198 | 199 |
200 | linalg.vector.new = function(values) 201 | 202 | Creates a new column vector 203 | 204 | **Parameters:** 205 | - `values` (`array`) 206 | The values to have for the column vector 207 | 208 | **Returns:** 209 | [t:(n x 1) matrix] The new column vector 210 | 211 |
212 | 213 |
214 | linalg.vector.e = function(i, n) 215 | 216 | Creates the standard basis vector i for R^n 217 | That is, creates a vector of length n with all zeros except at index i which will have value 1 218 | 219 | **Parameters:** 220 | - `i` (`number`) 221 | The index of e 222 | - `n` (`number`) 223 | The dimensionality of the vector 224 | 225 | **Returns:** 226 | [t:(n x 1) matrix] The standard basis vector e_i in R^n 227 | 228 |
229 | 230 | #### Vector Norm Functions 231 | 232 |
233 | linalg.vector.norm.l1 = function(v) 234 | 235 | The L1 norm of a vector 236 | sum_i{|v_i|} 237 | 238 | **Parameters:** 239 | - `[t:(n` 240 | x 1) matrix] v The vector 241 | 242 | **Returns:** 243 | `number` 244 | The resulting value 245 | 246 |
247 | 248 |
249 | linalg.vector.norm.l2 = function(v) 250 | 251 | The L2 norm of a vector 252 | sqrt(sum_i{(v_i)^2}) 253 | 254 | **Parameters:** 255 | - `[t:(n` 256 | x 1) matrix] v The vector 257 | 258 | **Returns:** 259 | `number` 260 | The resulting value 261 | 262 |
263 | 264 |
265 | linalg.vector.norm.linf = function(v) 266 | 267 | The L-infinity norm of a vector 268 | max{v} 269 | 270 | **Parameters:** 271 | - `[t:(n` 272 | x 1) matrix] v The vector 273 | 274 | **Returns:** 275 | `number` 276 | The resulting value 277 | 278 |
279 | 280 | #### Vector Inner Product Functions 281 | 282 |
283 | linalg.vector.ip.dot = function(v1, v2) 284 | 285 | Computes the standard dot product of two vectors 286 | Defined as \sum_{i=0}^{n-1} v1[i] * v2[i] 287 | 288 | **Parameters:** 289 | - `[t:(n` 290 | x 1) matrix] v1 The first vector 291 | - `[t:(n` 292 | x 1) matrix] v2 The second vector 293 | 294 | **Returns:** 295 | `number` 296 | The result 297 | 298 |
299 | 300 | #### Basic Vector Operation Functions 301 | 302 |
303 | linalg.vector.project = function (v, u, innerProductFunc, normFunc) 304 | 305 | Projects vector v onto vector space u 306 | Defined as \sum_{i=0}^{m-1} /|u[i]|^2 * u[i] 307 | 308 | **Parameters:** 309 | - `[t:(n` 310 | x 1) matrix] v The vector to project onto u 311 | - `[t:array<(n` 312 | x 1) matrix>] u The vector space to project v onto (can also be just one vector) 313 | - `[t:function([(n` 314 | x 1) matrix], [(n x 1) matrix])?] innerProductFunc The inner product function to use; Defaults to the dot product 315 | - `[t:function([(n` 316 | x 1) matrix])?] normFunc The norm function to use; Defaults to the L2 norm 317 | 318 | **Returns:** 319 | The vector projection of v onto u 320 | 321 |
322 | 323 |
324 | linalg.vector.unit = function(v, normFunc) 325 | 326 | Gets the unit vector with the same direction as the provided vectr 327 | 328 | **Parameters:** 329 | - `[t:(n` 330 | x 1) matrix] v The vector with the appropriate direction 331 | - `normFunc` (`function?`) 332 | The function to use as the norm; Defaults to the L2 norm 333 | 334 | **Returns:** 335 | [t:(n x 1) matrix] The unit vector 336 | 337 |
338 | 339 |
340 | linalg.vector.createArbitraryAxisRotationMatrix = function(v, theta) 341 | 342 | Creates a matrix that rotates a vector about an arbitrary vector 343 | Only works for 3 dimensions 344 | 345 | **Parameters:** 346 | - `[t:(n` 347 | x 1) matrix] v The vector to rotate about (should be a unit vector) 348 | - `theta` (`number`) 349 | The angle to rotate by (in radians) 350 | 351 | **Returns:** 352 | [t:nxn matrix] The resulting linear operator 353 | 354 |
355 | 356 | #### Vector Space Functions 357 | 358 |
359 | linalg.gramSchmidt = function (u, epsilon, dim, innerProductFunc, normFunc) 360 | 361 | Creates an orthonormal basis for a dim-dimensional inner product space 362 | 363 | **Parameters:** 364 | - `[t:array<(n` 365 | x 1) matrix>] u The list of matrices to add to the basis (will be converted to unit vectors) (can be a single vector instead of an array) 366 | - `epsilon` (`number?`) 367 | The minimum norm value for a vector to count to be added to the basis; Defaults to 0.01 368 | - `[t:function([(n` 369 | x 1) matrix], [(n x 1) matrix])?] innerProductFunc The inner product function to use; Defaults to the dot product 370 | - `[t:function([(n` 371 | x 1) matrix])?] normFunc The norm function to use; Defaults to the L2 norm 372 | 373 | **Returns:** 374 | [t:array<(n x 1) matrix>] An orthonormal basis that includes the unit vectors of the original u 375 | 376 |
-------------------------------------------------------------------------------- /foreman.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | rojo = { source = "rojo-rbx/rojo", version = "6" } 3 | run-in-roblox = { source = "rojo-rbx/run-in-roblox", version = "0.3.0" } 4 | wally = { source = "UpliftGames/wally", version = "0.2.1" } 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | toml == 0.10.2 -------------------------------------------------------------------------------- /roblox-testez.toml: -------------------------------------------------------------------------------- 1 | [selene] 2 | base = "roblox" 3 | name = "roblox-testez" 4 | 5 | [describe] 6 | [[describe.args]] 7 | type = "string" 8 | required = true 9 | 10 | [[describe.args]] 11 | type = "function" 12 | required = true 13 | 14 | [[it.args]] 15 | type = "string" 16 | required = true 17 | 18 | [[it.args]] 19 | type = "function" 20 | required = true 21 | 22 | [[expect.args]] 23 | type = "any" 24 | required = true 25 | -------------------------------------------------------------------------------- /scripts/get_wally_version_string.py: -------------------------------------------------------------------------------- 1 | import toml 2 | 3 | wally_manifest = toml.load('wally.toml') 4 | print(wally_manifest['package']['version'], end='') 5 | -------------------------------------------------------------------------------- /scripts/upload_model.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | import requests 3 | 4 | parser = ArgumentParser() 5 | parser.add_argument("-a", "--assetid", dest="assetid", required=True) 6 | parser.add_argument("-f", "--file", dest="filepath", required=True) 7 | parser.add_argument("-r", "--roblosecurity", 8 | dest="roblosecurity", required=True) 9 | 10 | args = parser.parse_args() 11 | 12 | url = "https://data.roblox.com/Data/Upload.ashx?assetid=" + args.assetid 13 | cookies = {'.ROBLOSECURITY': args.roblosecurity} 14 | headers = { 15 | 'Content-Type': 'application/xml', 16 | 'User-Agent': 'roblox', 17 | 'x-csrf-token': ''} 18 | 19 | with open(args.filepath, 'rb') as file_reader: 20 | payload = file_reader.read() 21 | 22 | response = requests.post( 23 | url=url, 24 | data=payload, 25 | cookies=cookies, 26 | headers=headers) 27 | 28 | if response.status_code == 403: 29 | headers['x-csrf-token'] = response.headers['x-csrf-token'] 30 | response = requests.post( 31 | url=url, 32 | data=payload, 33 | cookies=cookies, 34 | headers=headers) 35 | 36 | if response.status_code != 200: 37 | raise Exception("Request did not succeed") 38 | -------------------------------------------------------------------------------- /selene.toml: -------------------------------------------------------------------------------- 1 | std = "roblox-testez" 2 | 3 | [config.unused_variable] 4 | allow_unused_self = true 5 | -------------------------------------------------------------------------------- /spec.server.lua: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService("ReplicatedStorage") 2 | 3 | local TestEZ = require(ReplicatedStorage.wally_packages.testez) 4 | 5 | local testRoots = { 6 | ReplicatedStorage.src, 7 | } 8 | local results = TestEZ.TestBootstrap:run(testRoots, TestEZ.Reporters.TextReporter) 9 | 10 | -- Did something go wrong? 11 | if #results.errors > 0 or results.failureCount > 0 then 12 | error("Tests failed") 13 | end 14 | -------------------------------------------------------------------------------- /src/linalg.lua: -------------------------------------------------------------------------------- 1 | local linalg = {} 2 | 3 | -- ARITHMETIC FUNCTIONS 4 | local abs = math.abs 5 | local floor = math.floor 6 | local sqrt = math.sqrt 7 | 8 | -- TRIG FUNCTIONS 9 | local cos = math.cos 10 | local sin = math.sin 11 | 12 | --[[ ROW CLASS ]]-- 13 | local _rowClass = {} 14 | local _newRow = function(values) 15 | local instance = setmetatable({}, _rowClass) 16 | 17 | rawset(instance, "_values", values) 18 | 19 | return instance 20 | end 21 | _rowClass.__index = function(row, key) 22 | if type(key) == "number" then 23 | return row._values[key] 24 | elseif key == "Length" then 25 | return #row._values 26 | end 27 | 28 | error("Unrecognized key: " .. tostring(key)) 29 | end 30 | _rowClass.__newindex = function() 31 | error("Rows are immutable", 2) 32 | end 33 | _rowClass.__tostring = function(row) 34 | local result = "" 35 | 36 | for i = 1, row.Length do 37 | result = result .. row[i] 38 | if i < row.Length then 39 | result = result .. " " 40 | end 41 | end 42 | 43 | return result 44 | end 45 | _rowClass.__unm = function (row) 46 | if row.Length == 1 then 47 | return -1 * row[1] 48 | end 49 | 50 | local resultArray = {} 51 | 52 | for i = 1, row.Length do 53 | resultArray[i] = -1 * row[i] 54 | end 55 | 56 | return _newRow(resultArray) 57 | end 58 | _rowClass.__add = function (left, right) 59 | if type(left) == "number" or type(right) == "number" then 60 | local row = type(left) == "number" and right or left 61 | 62 | if row.Length == 1 then 63 | local leftScalar = type(left) == "number" and left or left[1] 64 | local rightScalar = type(right) == "number" and right or right[1] 65 | return leftScalar + rightScalar 66 | end 67 | 68 | error("Addition of a scalar to a row is undefined", 2) 69 | end 70 | 71 | if left.Length ~= right.Length then 72 | error("Addition of rows of different lengths is undefined", 2) 73 | end 74 | 75 | if left.Length == 1 then 76 | return left[1] + right[1] 77 | end 78 | 79 | local resultArray = {} 80 | 81 | for i = 1, left.Length do 82 | resultArray[i] = left[i] + right[i] 83 | end 84 | 85 | return _newRow(resultArray) 86 | end 87 | _rowClass.__sub = function (left, right) 88 | if type(left) == "number" or type(right) == "number" then 89 | local row = type(left) == "number" and right or left 90 | 91 | if row.Length == 1 then 92 | local leftScalar = type(left) == "number" and left or left[1] 93 | local rightScalar = type(right) == "number" and right or right[1] 94 | return leftScalar - rightScalar 95 | end 96 | 97 | error("Subtraction of a scalar to a row is undefined", 2) 98 | end 99 | 100 | if left.Length ~= right.Length then 101 | error("Subtraction of rows of different lengths is undefined", 2) 102 | end 103 | 104 | if left.Length == 1 then 105 | return left[1] - right[1] 106 | end 107 | 108 | local resultArray = {} 109 | 110 | for i = 1, left.Length do 111 | resultArray[i] = left[i] - right[i] 112 | end 113 | 114 | return _newRow(resultArray) 115 | end 116 | _rowClass.__mul = function (left, right) 117 | if type(left) == "number" or type(right) == "number" then 118 | local row = type(left) == "number" and right or left 119 | local scalar = type(left) == "number" and left or right 120 | 121 | if row.Length == 1 then 122 | local leftScalar = type(left) == "number" and left or left[1] 123 | local rightScalar = type(right) == "number" and right or right[1] 124 | return leftScalar * rightScalar 125 | else 126 | local resultArray = {} 127 | 128 | for i = 1, row.Length do 129 | resultArray[i] = scalar * row[i] 130 | end 131 | 132 | return _newRow(resultArray) 133 | end 134 | elseif left.Length == 1 or right.Length == 1 then 135 | if left.Length == 1 and right.Length == 1 then 136 | return left[1] * right[1] 137 | elseif left.Length == 1 then 138 | return left[1] * right 139 | else 140 | return left * right[1] 141 | end 142 | end 143 | 144 | error("Multiplication of two rows is undefined", 2) 145 | end 146 | _rowClass.__div = function (left, right) 147 | if type(left) == "number" then 148 | error("Division of a scalar by a row is undefined", 2) 149 | elseif type(right) == "number" then 150 | if left.Length == 1 then 151 | return left[1] / right 152 | else 153 | local resultArray = {} 154 | 155 | for i = 1, left.Length do 156 | resultArray[i] = left[i] / right 157 | end 158 | 159 | return _newRow(resultArray) 160 | end 161 | elseif left.Length == 1 or right.Length == 1 then 162 | if left.Length == 1 and right.Length == 1 then 163 | return left[1] / right[1] 164 | elseif left.Length == 1 then 165 | error("Division of a scalar by a row is undefined", 2) 166 | else 167 | return left / right[1] 168 | end 169 | end 170 | 171 | error("Division of two rows is undefined", 2) 172 | end 173 | _rowClass.__mod = function (left, right) 174 | if type(left) == "number" then 175 | error("Modulo of a scalar by a row is undefined", 2) 176 | elseif type(right) == "number" then 177 | if left.Length == 1 then 178 | return left[1] % right 179 | else 180 | local resultArray = {} 181 | 182 | for i = 1, left.Length do 183 | resultArray[i] = left[i] % right 184 | end 185 | 186 | return _newRow(resultArray) 187 | end 188 | elseif left.Length == 1 or right.Length == 1 then 189 | if left.Length == 1 and right.Length == 1 then 190 | return left[1] % right[1] 191 | elseif left.Length == 1 then 192 | error("Modulo of a scalar by a row is undefined", 2) 193 | else 194 | return left % right[1] 195 | end 196 | end 197 | 198 | error("Modulo of two rows is undefined", 2) 199 | end 200 | _rowClass.__pow = function (left, right) 201 | if type(left) == "number" then 202 | if right.Length == 1 then 203 | return left ^ right[1] 204 | else 205 | error("Exponentiation of a scalar by a row is undefined", 2) 206 | end 207 | elseif type(right) == "number" then 208 | if left.Length == 1 then 209 | return left[1] ^ right 210 | else 211 | error("Exponentiation of a row by a scalar is undefined", 2) 212 | end 213 | elseif left.Length == 1 or right.Length == 1 then 214 | if left.Length == 1 and right.Length == 1 then 215 | return left[1] ^ right[1] 216 | elseif left.Length == 1 then 217 | error("Exponentiation of a scalar by a row is undefined", 2) 218 | else 219 | error("Exponentiation of a row by a scalar is undefined", 2) 220 | end 221 | end 222 | 223 | error("Exponentiation of two rows is undefined", 2) 224 | end 225 | _rowClass.__eq = function(left, right) 226 | if left.Length ~= right.Length then 227 | return false 228 | end 229 | 230 | for i = 1, left.Length do 231 | if left[i] ~= right[i] then 232 | return false 233 | end 234 | end 235 | 236 | return true 237 | end 238 | 239 | --[[ MATRIX CLASS ]]-- 240 | local _matrixClass = {} 241 | local _newMatrix = function(rows) 242 | local rowInstances = {} 243 | for i = 1, #rows do 244 | rowInstances[i] = _newRow(rows[i]) 245 | end 246 | 247 | local instance = setmetatable({}, _matrixClass) 248 | rawset(instance, "_rows", rowInstances) 249 | 250 | return instance 251 | end 252 | _matrixClass.__index = function(mat, key) 253 | if type(key) == "number" then 254 | return mat._rows[key] 255 | elseif key == "Shape" then 256 | return { #mat._rows, mat._rows[1].Length } 257 | elseif key == "T" then 258 | return linalg.matrix.transpose(mat) 259 | elseif key == "Unit" then 260 | if mat.Shape[2] == 1 then 261 | return linalg.vector.unit(mat) 262 | elseif mat.Shape[1] == mat.Shape[2] then 263 | return linalg.matrix.identity(mat.Shape[1]) 264 | else 265 | error("Unit of a non-square matrix is undefined", 2) 266 | end 267 | elseif linalg.matrix[key] then 268 | if type(linalg.matrix[key]) == "function" then 269 | return function (...) return linalg.matrix[key](mat, ...) end 270 | end 271 | elseif linalg.vector[key] and mat.Shape[2] == 1 then 272 | if type(linalg.vector[key]) == "function" then 273 | return function (...) return linalg.vector[key](mat, ...) end 274 | end 275 | end 276 | 277 | error("Unrecognized key: " .. tostring(key)) 278 | end 279 | _matrixClass.__newindex = function() 280 | error("Matrices are immutable", 2) 281 | end 282 | _matrixClass.__tostring = function(mat) 283 | local result = "" 284 | 285 | for i = 1, mat.Shape[1] do 286 | result = result .. tostring(mat[i]) 287 | if i < mat.Shape[1] then 288 | result = result .. "\n" 289 | end 290 | end 291 | 292 | return result 293 | end 294 | _matrixClass.__unm = function(mat) 295 | local resultRows = {} 296 | 297 | for i = 1, mat.Shape[1] do 298 | resultRows[i] = {} 299 | for j = 1, mat.Shape[2] do 300 | resultRows[i][j] = -1 * mat[i][j] 301 | end 302 | end 303 | 304 | return _newMatrix(resultRows) 305 | end 306 | _matrixClass.__add = function(left, right) 307 | if left.Shape[1] ~= right.Shape[1] or left.Shape[2] ~= right.Shape[2] then 308 | error( 309 | "Cannot add matrices of sizes (" .. 310 | left.Shape[1] .. " x " .. left.Shape[2] .. ") and (" .. right.Shape[1] .. " x " .. right.Shape[2] .. ")", 311 | 2 312 | ) 313 | end 314 | 315 | local resultRows = {} 316 | 317 | for i = 1, left.Shape[1] do 318 | resultRows[i] = {} 319 | for j = 1, left.Shape[2] do 320 | resultRows[i][j] = left[i][j] + right[i][j] 321 | end 322 | end 323 | 324 | return _newMatrix(resultRows) 325 | end 326 | _matrixClass.__sub = function(left, right) 327 | if left.Shape[1] ~= right.Shape[1] or left.Shape[2] ~= right.Shape[2] then 328 | error( 329 | "Cannot subtract matrices of sizes (" .. 330 | left.Shape[1] .. " x " .. left.Shape[2] .. ") and (" .. right.Shape[1] .. " x " .. right.Shape[2] .. ")", 331 | 2 332 | ) 333 | end 334 | 335 | local resultRows = {} 336 | 337 | for i = 1, left.Shape[1] do 338 | resultRows[i] = {} 339 | for j = 1, left.Shape[2] do 340 | resultRows[i][j] = left[i][j] - right[i][j] 341 | end 342 | end 343 | 344 | return _newMatrix(resultRows) 345 | end 346 | _matrixClass.__mul = function(left, right) 347 | local resultRows = {} 348 | 349 | if type(left) == "number" or type(right) == "number" then 350 | local mat = type(left) == "number" and right or left 351 | local scalar = type(left) == "number" and left or right 352 | 353 | for i = 1, mat.Shape[1] do 354 | resultRows[i] = {} 355 | for j = 1, mat.Shape[2] do 356 | resultRows[i][j] = scalar * mat[i][j] 357 | end 358 | end 359 | else 360 | if left.Shape[2] ~= right.Shape[1] then 361 | error( 362 | "Cannot multiply matrices of sizes (" .. 363 | left.Shape[1] .. " x " .. left.Shape[2] .. ") and (" .. right.Shape[1] .. " x " .. right.Shape[2] .. ")", 364 | 2 365 | ) 366 | end 367 | 368 | for i = 1, left.Shape[1] do 369 | resultRows[i] = {} 370 | for j = 1, right.Shape[2] do 371 | resultRows[i][j] = 0 372 | for k = 1, left.Shape[2] do 373 | resultRows[i][j] = resultRows[i][j] + (left[i][k] * right[k][j]) 374 | end 375 | end 376 | end 377 | end 378 | 379 | return _newMatrix(resultRows) 380 | end 381 | _matrixClass.__div = function(mat, scalar) 382 | if type(mat) == "number" then 383 | error("Cannot divide a scalar by a matrix", 2) 384 | elseif type(scalar) ~= "number" then 385 | error("Cannot divide a matrix by a matrix", 2) 386 | end 387 | 388 | local resultRows = {} 389 | 390 | for i = 1, mat.Shape[1] do 391 | resultRows[i] = {} 392 | for j = 1, mat.Shape[2] do 393 | resultRows[i][j] = mat[i][j] / scalar 394 | end 395 | end 396 | 397 | return _newMatrix(resultRows) 398 | end 399 | _matrixClass.__mod = function(mat, scalar) 400 | if type(mat) == "number" then 401 | error("Cannot compute modulus of a scalar by a matrix", 2) 402 | elseif type(scalar) ~= "number" then 403 | error("Cannot compute modulus of a matrix by a matrix", 2) 404 | end 405 | 406 | local resultRows = {} 407 | 408 | for i = 1, mat.Shape[1] do 409 | resultRows[i] = {} 410 | for j = 1, mat.Shape[2] do 411 | resultRows[i][j] = mat[i][j] % scalar 412 | end 413 | end 414 | 415 | return _newMatrix(resultRows) 416 | end 417 | _matrixClass.__pow = function(base, power) 418 | if type(base) ~= "number" and type(power) == "number" then 419 | if base.Shape[1] ~= base.Shape[2] then 420 | error("Cannot exponentiate a non-square matrix", 2) 421 | end 422 | if floor(power) ~= power or power < 1 then 423 | error("Cannot exponentiate a matrix by a non-positive non-integer", 2) 424 | end 425 | 426 | -- Runs in O(n^2 log_2(n)) time 427 | -- See https://www.hackerearth.com/practice/notes/matrix-exponentiation-1/ 428 | local resultMatrix = linalg.matrix.identity(base.Shape[1]) 429 | local curMat = base 430 | 431 | while power > 0 do 432 | if power % 2 == 1 then 433 | resultMatrix = resultMatrix * curMat 434 | end 435 | 436 | curMat = curMat * curMat 437 | power = floor(power / 2) 438 | end 439 | 440 | return resultMatrix 441 | elseif type(base) == "number" and type(power) ~= "number" then 442 | error("Cannot raise an arbitrary number to a matrix power", 2) 443 | else 444 | error("Cannot exponentiate a matrix by a matrix", 2) 445 | --TODO (You should be able to do exactly this) 446 | end 447 | end 448 | _matrixClass.__eq = function(left, right) 449 | if left.Shape[1] ~= right.Shape[1] or left.Shape[2] ~= right.Shape[2] then 450 | return false 451 | end 452 | 453 | for i = 1, left.Shape[1] do 454 | for j = 1, left.Shape[2] do 455 | if left[i][j] ~= right[i][j] then 456 | return false 457 | end 458 | end 459 | end 460 | 461 | return true 462 | end 463 | 464 | -- MATRIX FUNCTIONS 465 | linalg.matrix = {} 466 | 467 | --[[** 468 | Creates a new matrix 469 | 470 | @param [t:array>] rows A (m x n) array of numbers to fill the matrix with 471 | 472 | @returns [t:(m x n) matrix] The new matrix 473 | **--]] 474 | linalg.matrix.new = function(rows) 475 | return _newMatrix(rows) 476 | end 477 | 478 | linalg.matrix.fromColumns = function(columnVectors) 479 | local resultRows = {} 480 | local n = columnVectors[1]["Shape"] and columnVectors[1].Shape[1] or #columnVectors[1] 481 | 482 | for i = 1, #columnVectors do 483 | for j = 1, n do 484 | if not resultRows[j] then 485 | resultRows[j] = {} 486 | end 487 | 488 | resultRows[j][i] = columnVectors[i][j] * 1 --*1 to ensure scalar functionality 489 | end 490 | end 491 | 492 | return _newMatrix(resultRows) 493 | end 494 | 495 | --[[** 496 | Creates an identity matrix of size (n x n) 497 | 498 | @param [t:number] n The size of the matrix 499 | 500 | @returns [t:(n x n) matrix] The identity matrix 501 | **--]] 502 | linalg.matrix.identity = function(n) 503 | local rows = {} 504 | 505 | for i = 1, n do 506 | rows[i] = {} 507 | for j = 1, n do 508 | rows[i][j] = i == j and 1 or 0 509 | end 510 | end 511 | 512 | return _newMatrix(rows) 513 | end 514 | 515 | --[[** 516 | Creates a matrix of all zeros of size (m x n) 517 | 518 | @param [t:number] m 519 | @param [t:number] n 520 | 521 | @returns [t:(m x n) matrix] The zeros matrix 522 | **--]] 523 | linalg.matrix.zeros = function(m, n) 524 | local rows = {} 525 | 526 | for i = 1, m do 527 | rows[i] = {} 528 | for j = 1, n do 529 | rows[i][j] = 0 530 | end 531 | end 532 | 533 | return _newMatrix(rows) 534 | end 535 | 536 | --[[** 537 | Creates a matrix of all zeros of the same size as the provided matrix 538 | 539 | @param [t:(m x n) matrix] mat The matrix to copy the size of 540 | 541 | @returns [t:(m x n) matrix] The zeros matrix 542 | **--]] 543 | linalg.matrix.zerosLike = function(mat) 544 | return linalg.matrix.zeros(mat.Shape[1], mat.Shape[2]) 545 | end 546 | 547 | --[[** 548 | Creates a diagonal matrix with the values provided as the diagonal entries 549 | 550 | @param [t:array] values An n-length array whose entries will be set as the diagonal entries 551 | 552 | @returns [t:(n x n) matrix] The resulting diagonal matrix 553 | **--]] 554 | linalg.matrix.diagonal = function(values) 555 | local resultRows = {} 556 | 557 | for i = 1, #values do 558 | resultRows[i] = {} 559 | for j = 1, #values do 560 | resultRows[i][j] = i == j and values[i] or 0 561 | end 562 | end 563 | 564 | return _newMatrix(resultRows) 565 | end 566 | 567 | --[[** 568 | Determines whether a matrix is diagonal 569 | Does not exclusively refer to square matrices 570 | Refers strictly to whether all non-zero values are on the main diagonal (i.e., a_{ij} = 0 for all i, j where i ~= j) 571 | 572 | @param [t:(m x n) matrix] mat The matrix to check 573 | 574 | @returns [t:boolean] True if the matrix is diagonal, false otherwise 575 | **--]] 576 | linalg.matrix.isDiagonal = function(mat) 577 | for i = 1, mat.Shape[1] do 578 | for j = 1, mat.Shape[2] do 579 | if i ~= j and mat[i][j] ~= 0 then 580 | return false 581 | end 582 | end 583 | end 584 | 585 | return true 586 | end 587 | 588 | --[[** 589 | Determines whether a matrix is upper triangular 590 | Note that any non-square matrix will return false 591 | 592 | @param [t:(m x n) matrix] mat The matrix to check 593 | 594 | @returns [t:boolean] True if the matrix is upper triangular, false otherwise 595 | **--]] 596 | linalg.matrix.isUpperTriangular = function(mat) 597 | if mat.Shape[1] ~= mat.Shape[2] then 598 | return false 599 | end 600 | 601 | for i = 1, mat.Shape[1] do 602 | for j = 1, mat.Shape[2] do 603 | if i > j and mat[i][j] ~= 0 then 604 | return false 605 | end 606 | end 607 | end 608 | 609 | return true 610 | end 611 | 612 | --[[** 613 | Determines whether a matrix is lower triangular 614 | Note that any non-square matrix will return false 615 | 616 | @param [t:(m x n) matrix] mat The matrix to check 617 | 618 | @returns [t:boolean] True if the matrix is lower triangular, false otherwise 619 | **--]] 620 | linalg.matrix.isLowerTriangular = function(mat) 621 | if mat.Shape[1] ~= mat.Shape[2] then 622 | return false 623 | end 624 | 625 | for i = 1, mat.Shape[1] do 626 | for j = 1, mat.Shape[2] do 627 | if i < j and mat[i][j] ~= 0 then 628 | return false 629 | end 630 | end 631 | end 632 | 633 | return true 634 | end 635 | 636 | --[[** 637 | Creates a new matrix that is the transpose of the provided matrix 638 | 639 | @param [t: (m x n) matrix] mat The matrix to create the transpose of 640 | 641 | @returns [t: (n x m) matrix] The transpose of mat 642 | **--]] 643 | linalg.matrix.transpose = function(mat) 644 | local resultRows = {} 645 | 646 | for i = 1, mat.Shape[1] do 647 | for j = 1, mat.Shape[2] do 648 | if not resultRows[j] then 649 | resultRows[j] = {} 650 | end 651 | 652 | resultRows[j][i] = mat[i][j] 653 | end 654 | end 655 | 656 | return _newMatrix(resultRows) 657 | end 658 | 659 | --[[** 660 | Solves for e^mat 661 | Defined as: e^A = \sum_{k=0}^{\infinity} \frac{1}{k!} A^k 662 | Implemented in a naive way to approximate by using iterations 663 | Runtime of O(n^3) 664 | 665 | @param [t:(n x n) matrix] mat The matrix to use as the exponent 666 | @param [t:number] numIterations The number of iterations to take the sum of the taylor series to 667 | 668 | @returns The matrix exponential approximation 669 | **--]] 670 | linalg.matrix.expm = function(mat, numIterations) 671 | if mat.Shape[1] ~= mat.Shape[2] then 672 | error("Cannot find the exponential of a non-square matrix", 2) 673 | end 674 | 675 | if not numIterations then 676 | numIterations = 128 677 | end 678 | 679 | local resultMatrix = linalg.matrix.identity(mat.Shape[1]) 680 | local curMat = resultMatrix 681 | local factorial = 1 682 | 683 | for i = 1, numIterations do 684 | curMat = curMat * mat 685 | factorial = factorial * i 686 | 687 | resultMatrix = resultMatrix + ((1 / factorial) * curMat) 688 | end 689 | 690 | return resultMatrix 691 | end 692 | 693 | -- VECTOR FUNCTIONS 694 | linalg.vector = {} 695 | 696 | --[[** 697 | Creates a new column vector 698 | 699 | @param [t:array] values The values to have for the column vector 700 | 701 | @returns [t:(n x 1) matrix] The new column vector 702 | **--]] 703 | linalg.vector.new = function(values) 704 | local rows = {} 705 | 706 | for i = 1, #values do 707 | rows[i] = {values[i]} 708 | end 709 | 710 | return _newMatrix(rows) 711 | end 712 | 713 | --[[** 714 | Creates the standard basis vector i for R^n 715 | That is, creates a vector of length n with all zeros except at index i which will have value 1 716 | 717 | @param [t:number] i The index of e 718 | @param [t:number] n The dimensionality of the vector 719 | 720 | @returns [t:(n x 1) matrix] The standard basis vector e_i in R^n 721 | **--]] 722 | linalg.vector.e = function(i, n) 723 | local rows = {} 724 | 725 | for j = 1, n do 726 | rows[j] = i == j and {1} or {0} 727 | end 728 | 729 | return _newMatrix(rows) 730 | end 731 | 732 | linalg.vector.norm = {} 733 | 734 | --[[** 735 | The L1 norm of a vector 736 | sum_i{|v_i|} 737 | 738 | @param [t:(n x 1) matrix] v The vector 739 | 740 | @returns [t:number] The resulting value 741 | **--]] 742 | linalg.vector.norm.l1 = function(v) 743 | local sum = 0 744 | 745 | for i = 1, v.Shape[1] do 746 | sum = sum + abs(v[i][1]) 747 | end 748 | 749 | return sum 750 | end 751 | 752 | --[[** 753 | The L2 norm of a vector 754 | sqrt(sum_i{(v_i)^2}) 755 | 756 | @param [t:(n x 1) matrix] v The vector 757 | 758 | @returns [t:number] The resulting value 759 | **--]] 760 | linalg.vector.norm.l2 = function(v) 761 | local sum = 0 762 | 763 | for i = 1, v.Shape[1] do 764 | sum = sum + v[i]^2 765 | end 766 | 767 | return sqrt(sum) 768 | end 769 | 770 | --[[** 771 | The L-infinity norm of a vector 772 | max{v} 773 | 774 | @param [t:(n x 1) matrix] v The vector 775 | 776 | @returns [t:number] The resulting value 777 | **--]] 778 | linalg.vector.norm.linf = function(v) 779 | local maxComponentValue = 0 780 | 781 | for i = 1, v.Shape[1] do 782 | local componentValue = abs(v[i][1]) 783 | if componentValue > maxComponentValue then 784 | maxComponentValue = componentValue 785 | end 786 | end 787 | 788 | return maxComponentValue 789 | end 790 | 791 | -- Inner product functions 792 | linalg.vector.ip = {} 793 | 794 | --[[** 795 | Computes the standard dot product of two vectors 796 | Defined as \sum_{i=0}^{n-1} v1[i] * v2[i] 797 | 798 | @param [t:(n x 1) matrix] v1 The first vector 799 | @param [t:(n x 1) matrix] v2 The second vector 800 | 801 | @returns [t:number] The result 802 | **--]] 803 | linalg.vector.ip.dot = function(v1, v2) 804 | if v1.Shape[1] ~= v2.Shape[1] then 805 | error("Cannot compute the dot product for vectors of unequal dimensions", 2) 806 | end 807 | if v1.Shape[2] ~= 1 or v2.Shape[2] ~= 1 then 808 | error("Cannot compute the dot product for non column vectors", 2) 809 | end 810 | 811 | local result = 0 812 | 813 | for i = 1, v1.Shape[1] do 814 | result = result + (v1[i] * v2[i]) 815 | end 816 | 817 | return result 818 | end 819 | 820 | --[[** 821 | Projects vector v onto vector space u 822 | Defined as \sum_{i=0}^{m-1} /|u[i]|^2 * u[i] 823 | 824 | @param [t:(n x 1) matrix] v The vector to project onto u 825 | @param [t:array<(n x 1) matrix>] u The vector space to project v onto (can also be just one vector) 826 | @param [t:function([(n x 1) matrix], [(n x 1) matrix])?] innerProductFunc The inner product function to use; Defaults to the dot product 827 | @param [t:function([(n x 1) matrix])?] normFunc The norm function to use; Defaults to the L2 norm 828 | 829 | @returns The vector projection of v onto u 830 | **--]] 831 | linalg.vector.project = function (v, u, innerProductFunc, normFunc) 832 | if #u == 0 then -- u must be a single vector 833 | u = { u } 834 | end 835 | 836 | if u[1].Shape[1] ~= v.Shape[1] then 837 | error("Cannot project vectors of different dimensions", 2) 838 | end 839 | if u[1].Shape[2] ~= 1 or v.Shape[2] ~= 1 then 840 | error("Expected two vectors, not matrices", 2) 841 | end 842 | 843 | if not innerProductFunc then 844 | innerProductFunc = linalg.vector.ip.dot 845 | end 846 | if not normFunc then 847 | normFunc = linalg.vector.norm.l2 848 | end 849 | 850 | local result = linalg.matrix.zerosLike(v) 851 | 852 | for i = 1, #u do 853 | result = result + (innerProductFunc(v, u[i]) / normFunc(u[i])^2) * u[i] 854 | end 855 | 856 | return result 857 | end 858 | 859 | --[[** 860 | Gets the unit vector with the same direction as the provided vectr 861 | 862 | @param [t:(n x 1) matrix] v The vector with the appropriate direction 863 | @param [t:function?] normFunc The function to use as the norm; Defaults to the L2 norm 864 | 865 | @returns [t:(n x 1) matrix] The unit vector 866 | **--]] 867 | linalg.vector.unit = function(v, normFunc) 868 | if not normFunc then normFunc = linalg.vector.norm.l2 end 869 | return v / normFunc(v) 870 | end 871 | 872 | --[[** 873 | Creates a matrix that rotates a vector about an arbitrary vector 874 | Only works for 3 dimensions 875 | 876 | @param [t:(n x 1) matrix] v The vector to rotate about (should be a unit vector) 877 | @param [t:number] theta The angle to rotate by (in radians) 878 | 879 | @returns [t:nxn matrix] The resulting linear operator 880 | **--]] 881 | linalg.vector.createArbitraryAxisRotationMatrix = function(v, theta) 882 | if v.Shape[1] ~= 3 then 883 | error("Cannot create rotation matrix about arbitrary unit vector other than in R^3", 2) 884 | end 885 | 886 | local costheta = cos(theta) 887 | local sintheta = sin(theta) 888 | local versine = 1 - costheta 889 | local u = v.Unit 890 | 891 | return _newMatrix({ 892 | {versine * u[1] ^ 2 + costheta, (versine * u[1] * u[2]) - (sintheta * u[3]), (versine * u[1] * u[3]) + (sintheta * u[2])}, 893 | {(versine * u[1] * u[2]) + (sintheta * u[3]), versine * u[2] ^ 2 + costheta, (versine * u[2] * u[3]) - (sintheta * u[1])}, 894 | {(versine * u[1] * u[3]) - (sintheta * u[2]), (versine * u[2] * u[3]) + (sintheta * u[1]), versine * u[3] ^ 2 + costheta} 895 | }) 896 | end 897 | 898 | --[[ GENERAL FUNCTIONS ]]-- 899 | --[[** 900 | Creates an orthonormal basis for a dim-dimensional inner product space 901 | 902 | @param [t:array<(n x 1) matrix>] u The list of matrices to add to the basis (will be converted to unit vectors) (can be a single vector instead of an array) 903 | @param [t:number?] epsilon The minimum norm value for a vector to count to be added to the basis; Defaults to 0.01 904 | @param [t:function([(n x 1) matrix], [(n x 1) matrix])?] innerProductFunc The inner product function to use; Defaults to the dot product 905 | @param [t:function([(n x 1) matrix])?] normFunc The norm function to use; Defaults to the L2 norm 906 | 907 | @returns [t:array<(n x 1) matrix>] An orthonormal basis that includes the unit vectors of the original u 908 | **--]] 909 | linalg.gramSchmidt = function (u, epsilon, dim, innerProductFunc, normFunc) 910 | if #u == 0 then -- u must be just a single vector 911 | u = { u } 912 | end 913 | if not epsilon then 914 | epsilon = 0.01 915 | end 916 | if not dim then 917 | dim = u[1].Shape[1] 918 | end 919 | if not innerProductFunc then 920 | innerProductFunc = linalg.vector.ip.dot 921 | end 922 | if not normFunc then 923 | normFunc = linalg.vector.norm.l2 924 | end 925 | 926 | -- Normalize all elements in u 927 | local newU = {} 928 | for i = 1, #u do 929 | newU[i] = u[i].Unit 930 | end 931 | u = newU 932 | 933 | for i = 1, dim do 934 | local eiRows = {} 935 | for j = 1, dim do 936 | eiRows[j] = { i == j and 1 or 0 } 937 | end 938 | table.insert(u, _newMatrix(eiRows)) 939 | end 940 | 941 | local basisVectors = { u[1].Unit } 942 | 943 | for i = 1, #u do 944 | local potentialBasisVector = u[i] - linalg.vector.project(u[i], basisVectors, innerProductFunc, normFunc) 945 | if normFunc(potentialBasisVector) >= epsilon then 946 | table.insert(basisVectors, potentialBasisVector.Unit) 947 | end 948 | 949 | if #basisVectors == dim then 950 | break 951 | end 952 | end 953 | 954 | return basisVectors 955 | end 956 | 957 | return linalg 958 | -------------------------------------------------------------------------------- /src/linalg.spec.lua: -------------------------------------------------------------------------------- 1 | return function() 2 | local linalg = require(script.Parent.linalg) 3 | 4 | it("should have immutable matrices", function() 5 | local mat = linalg.matrix.new({ 6 | {1, 2}, 7 | {3, 4} 8 | }) 9 | 10 | expect(function() mat[1] = 1 end).to.throw() 11 | expect(function() mat[1][1] = 1 end).to.throw() 12 | end) 13 | 14 | it("should error on unrecognized keys", function() 15 | local mat = linalg.matrix.new({ 16 | {1, 2}, 17 | {3, 4} 18 | }) 19 | 20 | expect(function() local _ = mat["Invalid Key"] end).to.throw() 21 | expect(function() local _ = mat[1]["Invalid Key"] end).to.throw() 22 | end) 23 | 24 | it("should handle equality correctly", function() 25 | local mat = linalg.matrix.new({ 26 | {1, 2}, 27 | {3, 4} 28 | }) 29 | local matT = linalg.matrix.new({ 30 | {1, 3}, 31 | {2, 4} 32 | }) 33 | local mat2x3 = linalg.matrix.new({ 34 | {1, 2, 3}, 35 | {4, 5, 6} 36 | }) 37 | 38 | expect(mat == mat).to.equal(true) 39 | expect(mat == matT).to.equal(false) 40 | expect(mat == mat2x3).to.equal(false) 41 | expect(mat[1] == mat[1]).to.equal(true) 42 | expect(mat[1] == matT[1]).to.equal(false) 43 | expect(mat[1] == mat2x3[1]).to.equal(false) 44 | end) 45 | 46 | it("should handle matrix and scalar arithmetic correctly", function() 47 | local mat = linalg.matrix.new({ 48 | {1, 2}, 49 | {3, 4} 50 | }) 51 | local mat2x3 = linalg.matrix.new({ 52 | {1, 2, 3}, 53 | {4, 5, 6} 54 | }) 55 | 56 | expect(-mat).to.equal(linalg.matrix.new({ 57 | {-1, -2}, 58 | {-3, -4} 59 | })) 60 | 61 | expect(function() local _ = 1 + mat end).to.throw() 62 | expect(function() local _ = mat + 2 end).to.throw() 63 | 64 | expect(function() local _ = 1 - mat end).to.throw() 65 | expect(function() local _ = mat - 2 end).to.throw() 66 | 67 | expect(2 * mat).to.equal(linalg.matrix.new({ 68 | {2, 4}, 69 | {6, 8} 70 | })) 71 | expect(2 * mat).to.equal(mat * 2) 72 | 73 | expect(mat / 2).to.equal(linalg.matrix.new({ 74 | {0.5, 1}, 75 | {1.5, 2} 76 | })) 77 | expect(function() local _ = 2 / mat end).to.throw() 78 | expect(function() local _ = mat / mat end).to.throw() 79 | 80 | expect(mat % 2).to.equal(linalg.matrix.new({ 81 | {1, 0}, 82 | {1, 0} 83 | })) 84 | expect(function() local _ = 1 % mat end).to.throw() 85 | expect(function() local _ = mat % mat end).to.throw() 86 | 87 | expect(mat ^ 2).to.equal(mat * mat) 88 | expect(function() local _ = mat2x3 ^ 2 end).to.throw() 89 | expect(function() local _ = mat ^ 2.5 end).to.throw() 90 | expect(function() local _ = 2 ^ mat end).to.throw() 91 | expect(function() local _ = mat ^ mat end).to.throw() 92 | end) 93 | 94 | it("should handle matrix and matrix arithmetic correctly", function() 95 | local mat = linalg.matrix.new({ 96 | {1, 2}, 97 | {3, 4} 98 | }) 99 | 100 | expect(mat + mat).to.equal(mat * 2) 101 | expect(function() local _ = mat + linalg.matrix.new({ {1} }) end).to.throw() 102 | 103 | expect(mat - mat).to.equal(linalg.matrix.zeros(2, 2)) 104 | expect(function() local _ = mat - linalg.matrix.new({ {1} }) end).to.throw() 105 | 106 | expect(mat * mat).to.equal(linalg.matrix.new({ 107 | {7, 10}, 108 | {15, 22} 109 | })) 110 | expect(function() local _ = mat * linalg.matrix.new({ {1} }) end).to.throw() 111 | end) 112 | 113 | it("should handle arithmetic on rows correctly", function() 114 | local mat = linalg.matrix.new({ 115 | {1, 2}, 116 | {3, 4} 117 | }) 118 | local v = linalg.vector.new({ 1, 2 }) 119 | 120 | expect(-(mat[1])).to.equal(linalg.matrix.new({ {-1, -2} })[1]) 121 | expect(-v).to.equal(linalg.vector.new({ -1, -2 })) 122 | expect(-v[1]).to.equal(-1) 123 | 124 | expect(v[1] + 1).to.equal(2) 125 | expect(v[1] + v[1]).to.equal(2) 126 | expect(mat[1] + mat[1]).to.equal(linalg.matrix.new({ {2, 4} })[1]) 127 | expect(function() local _ = mat[1] + 1 end).to.throw() 128 | expect(function() local _ = mat[1] + v[1] end).to.throw() 129 | 130 | expect(v[1] - 1).to.equal(0) 131 | expect(v[1] - v[1]).to.equal(0) 132 | expect(mat[1] - mat[1]).to.equal(linalg.matrix.new({ {0, 0} })[1]) 133 | expect(function() local _ = mat[1] - 1 end).to.throw() 134 | expect(function() local _ = mat[1] - v[1] end).to.throw() 135 | 136 | expect(2 * mat[1]).to.equal(linalg.matrix.new({ {2, 4} })[1]) 137 | expect(v[1] * mat[1]).to.equal(mat[1]) 138 | expect(mat[1] * v[1]).to.equal(mat[1]) 139 | expect(2 * v[1]).to.equal(2) 140 | expect(v[1] * 2).to.equal(2) 141 | expect(v[1] * v[1]).to.equal(1) 142 | expect(function() local _ = mat[1] * mat[1] end).to.throw() 143 | 144 | expect(mat[2] / 2).to.equal(linalg.matrix.new({ {1.5, 2} })[1]) 145 | expect(mat[1] / v[1]).to.equal(mat[1]) 146 | expect(v[2] / 2).to.equal(1) 147 | expect(v[1] / v[1]).to.equal(1) 148 | expect(function() local _ = 1 / mat[1] end).to.throw() 149 | expect(function() local _ = v[1] / mat[1] end).to.throw() 150 | expect(function() local _ = mat[1] / mat[1] end).to.throw() 151 | 152 | expect(mat[2] % 2).to.equal(linalg.matrix.new({ {1, 0} })[1]) 153 | expect(mat[1] % v[2]).to.equal(linalg.matrix.new({ {1, 0} })[1]) 154 | expect(v[1] % 2).to.equal(1) 155 | expect(v[1] % v[2]).to.equal(1) 156 | expect(function() local _ = 1 % mat[1] end).to.throw() 157 | expect(function() local _ = v[1] % mat[1] end).to.throw() 158 | expect(function() local _ = mat[1] % mat[1] end).to.throw() 159 | 160 | expect(2 ^ v[2]).to.equal(4) 161 | expect(v[2] ^ 2).to.equal(4) 162 | expect(v[2] ^ v[2]).to.equal(4) 163 | expect(function() local _ = 2 ^ mat[1] end).to.throw() 164 | expect(function() local _ = mat[1] ^ 2 end).to.throw() 165 | expect(function() local _ = v[2] ^ mat[1] end).to.throw() 166 | expect(function() local _ = mat[1] ^ v[2] end).to.throw() 167 | expect(function() local _ = mat[1] ^ mat[1] end).to.throw() 168 | end) 169 | 170 | it("should handle basic matrix operations correctly", function() 171 | local mat = linalg.matrix.new({ 172 | {1, 2}, 173 | {3, 4} 174 | }) 175 | local mat2x3 = linalg.matrix.new({ 176 | {1, 2, 3}, 177 | {4, 5, 6} 178 | }) 179 | 180 | expect(mat.T).to.equal(linalg.matrix.new({ 181 | {1, 3}, 182 | {2, 4} 183 | })) 184 | 185 | expect(mat.Unit).to.equal(linalg.matrix.new({ 186 | {1, 0}, 187 | {0, 1} 188 | })) 189 | expect(function() local _ = mat2x3.Unit end).to.throw() 190 | 191 | local expmResult = mat.expm() 192 | expect(expmResult[1][1]).to.be.near(51.968956198705) 193 | expect(expmResult[1][2]).to.be.near(74.736564567003) 194 | expect(expmResult[2][1]).to.be.near(112.1048468505) 195 | expect(expmResult[2][2]).to.be.near(164.07380304921) 196 | 197 | expect(function() local _ = linalg.matrix.new({ 198 | {1, 2, 3}, 199 | {4, 5, 6} 200 | }).expm() end).to.throw() 201 | end) 202 | 203 | it("should create matrices correctly", function() 204 | expect(linalg.matrix.fromColumns({{1, 3}, {2, 4}})).to.equal(linalg.matrix.new({ 205 | {1, 2}, 206 | {3, 4} 207 | })) 208 | expect(linalg.matrix.fromColumns({ 209 | linalg.matrix.new({ {1}, {3} }), 210 | linalg.matrix.new({ {2}, {4} }) 211 | })).to.equal(linalg.matrix.new({ 212 | {1, 2}, 213 | {3, 4} 214 | })) 215 | 216 | expect(linalg.matrix.identity(2)).to.equal(linalg.matrix.new({ 217 | {1, 0}, 218 | {0, 1} 219 | })) 220 | 221 | expect(linalg.matrix.zeros(2, 2)).to.equal(linalg.matrix.new({ 222 | {0, 0}, 223 | {0, 0} 224 | })) 225 | 226 | expect(linalg.matrix.zerosLike(linalg.matrix.identity(2))).to.equal(linalg.matrix.new({ 227 | {0, 0}, 228 | {0, 0} 229 | })) 230 | 231 | expect(linalg.matrix.diagonal({1, 2})).to.equal(linalg.matrix.new({ 232 | {1, 0}, 233 | {0, 2} 234 | })) 235 | end) 236 | 237 | it("should classify matrices correctly", function() 238 | expect(linalg.matrix.identity(2).isDiagonal()).to.equal(true) 239 | expect(linalg.matrix.new({ 240 | {1, 0, 0}, 241 | {0, 1, 0} 242 | }).isDiagonal()).to.equal(true) 243 | expect(linalg.matrix.new({ 244 | {1, 2}, 245 | {3, 4} 246 | }).isDiagonal()).to.equal(false) 247 | 248 | expect(linalg.matrix.identity(2).isUpperTriangular()).to.equal(true) 249 | expect(linalg.matrix.new({ 250 | {1, 2}, 251 | {3, 4} 252 | }).isUpperTriangular()).to.equal(false) 253 | expect(linalg.matrix.new({ 254 | {1, 2, 3}, 255 | {4, 5, 6} 256 | }).isUpperTriangular()).to.equal(false) 257 | 258 | expect(linalg.matrix.identity(2).isLowerTriangular()).to.equal(true) 259 | expect(linalg.matrix.new({ 260 | {1, 2}, 261 | {3, 4} 262 | }).isLowerTriangular()).to.equal(false) 263 | expect(linalg.matrix.new({ 264 | {1, 2, 3}, 265 | {4, 5, 6} 266 | }).isLowerTriangular()).to.equal(false) 267 | end) 268 | 269 | it("should calculate norms correctly", function() 270 | local v = linalg.vector.new({ 1, 2 }) 271 | 272 | expect(linalg.vector.norm.l1(v)).to.equal(3) 273 | 274 | expect(linalg.vector.norm.l2(v)).to.equal(math.sqrt(5)) 275 | 276 | expect(linalg.vector.norm.linf(v)).to.equal(2) 277 | end) 278 | 279 | it("should calculate inner products correctly", function() 280 | local I_2 = linalg.matrix.identity(2) 281 | local v1 = linalg.vector.new({ 1, 2 }) 282 | local v2 = linalg.vector.new({ 2, 1 }) 283 | local r3v = linalg.vector.new({ 1, 2, 3 }) 284 | 285 | expect(linalg.vector.ip.dot(v1, v2)).to.equal(4) 286 | expect(function() local _ = linalg.vector.ip.dot(v1, I_2) end).to.throw() 287 | expect(function() local _ = linalg.vector.ip.dot(v1, r3v) end).to.throw() 288 | end) 289 | 290 | it("should handle basic vector operations correctly", function() 291 | local I_2 = linalg.matrix.identity(2) 292 | local v = linalg.vector.new({ 1, 2 }) 293 | local e1 = linalg.vector.e(1, 2) 294 | local e2 = linalg.vector.e(2, 2) 295 | local r3v = linalg.vector.new({ 1, 2, 3 }) 296 | 297 | expect(v.project({e1, e2})).to.equal(v) 298 | expect(v.project(e1)).to.equal(e1) 299 | expect(function() local _ = v.project(I_2) end).to.throw() 300 | expect(function() local _ = v.project(r3v) end).to.throw() 301 | 302 | expect(e1.Unit).to.equal(e1) 303 | end) 304 | 305 | it("should create proper rotation matrices", function() 306 | local e1 = linalg.vector.e(1, 3) 307 | local e2 = linalg.vector.e(2, 3) 308 | local rot180 = e1.createArbitraryAxisRotationMatrix(math.pi) 309 | 310 | expect(rot180 * e1).to.equal(e1) 311 | 312 | do 313 | local rotatedE2 = rot180 * e2 314 | expect(rotatedE2[1][1]).to.equal(0) 315 | expect(rotatedE2[2][1]).to.be.near(-1) 316 | expect(rotatedE2[3][1]).to.be.near(0) 317 | end 318 | 319 | expect(function() local _ = linalg.vector.e(1, 2).createArbitraryAxisRotationMatrix(0) end).to.throw() 320 | end) 321 | 322 | it("should create a proper orthonormal basis", function() 323 | local e1 = linalg.vector.e(1, 2) 324 | local e2 = linalg.vector.e(2, 2) 325 | 326 | do 327 | local basis = linalg.gramSchmidt({ e1, e2 }) 328 | expect(#basis).to.equal(2) 329 | expect(basis[1]).to.equal(e1) 330 | expect(basis[2]).to.equal(e2) 331 | end 332 | 333 | do 334 | local basis = linalg.gramSchmidt(e1) 335 | expect(#basis).to.equal(2) 336 | expect(basis[1]).to.equal(e1) 337 | expect(basis[2]).to.equal(e2) 338 | end 339 | end) 340 | end -------------------------------------------------------------------------------- /standalone-model.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linalg", 3 | "tree": { 4 | "$path": "src" 5 | } 6 | } -------------------------------------------------------------------------------- /unit-tests.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unit-tests", 3 | "globIgnorePaths": [ 4 | "**/package.json", 5 | "**/tsconfig.json", 6 | "**/*.project.json", 7 | "**/.editorconfig", 8 | "**/.git*", 9 | "**/.vscode", 10 | "**/*.md", 11 | "**/*.toml", 12 | "**/*.yaml", 13 | "**/*.yml", 14 | "**/*.lock" 15 | ], 16 | "tree": { 17 | "$className": "DataModel", 18 | "ReplicatedStorage": { 19 | "$className": "ReplicatedStorage", 20 | "wally_packages": { 21 | "$path": "Packages" 22 | }, 23 | "src": { 24 | "$path": "./src" 25 | } 26 | }, 27 | "ServerScriptService": { 28 | "$className": "ServerScriptService", 29 | "TestRunner": { 30 | "$path": "./spec.server.lua" 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /wally.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Wally. 2 | # It is not intended for manual editing. 3 | registry = "test" 4 | 5 | [[package]] 6 | name = "bytebit/linalg" 7 | version = "1.0.0" 8 | dependencies = [["testez", "roblox/testez@0.4.1"]] 9 | 10 | [[package]] 11 | name = "roblox/testez" 12 | version = "0.4.1" 13 | dependencies = [] 14 | -------------------------------------------------------------------------------- /wally.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bytebit/linalg" 3 | description = "A simple script to implement linear algebra functions not provided by the Lua standard API, developed especially for use on Roblox" 4 | version = "1.0.0" 5 | license = "MIT" 6 | registry = "https://github.com/UpliftGames/wally-index" 7 | realm = "shared" 8 | 9 | [dependencies] 10 | testez = "roblox/testez@0.4.1" --------------------------------------------------------------------------------