├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
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"
--------------------------------------------------------------------------------