Compare commits

...
Author SHA1 Message Date
Cynopolis 5600b05b09 Working on an SVD implimentation 2026-08-14 09:19:56 -04:00
Cynopolis a49e357f4c Added additional constraints to list instantiation 2026-08-13 11:33:19 -04:00
Cynopolis ea29ea27f2 Removed some unused variables 2026-08-11 11:19:56 -04:00
Cynopolis 48b016d8b7 Merge pull request 'Updating the readme' (#7) from update-readme into main
Reviewed-on: #7
2025-06-30 19:05:53 +00:00
Cynopolis 8e4595f2ef Updated readme
Merge-Checker / build_and_test (pull_request) Successful in 1m10s
2025-06-30 14:52:48 -04:00
Cynopolis 99c0d3ed70 Merge pull request 'Adjusted timing test repetition and added QR decomposition' (#6) from Minor-cicd-fixes into main
Merge-Checker / build_and_test (pull_request) Successful in 1m13s
Reviewed-on: #6
2025-06-10 23:06:02 +00:00
Cynopolis 80c4ebfece Put time usage back
Merge-Checker / build_and_test (pull_request) Successful in 1m18s
2025-06-07 11:08:56 -04:00
Cynopolis 8b6f1de822 Updated timing test timings
Merge-Checker / build_and_test (pull_request) Successful in 1m17s
2025-06-07 11:03:55 -04:00
Cynopolis 719fc4d28a Adjusted timing test repetition and added QR decomposition
Merge-Checker / build_and_test (pull_request) Has been cancelled
2025-06-07 10:58:59 -04:00
Cynopolis 2a7eb93ebe Merge pull request 'Working on adding efficient eigenvector and value calculations' (#2) from eigenvector-and-values into main
Reviewed-on: #2
2025-06-06 22:32:18 +00:00
Cynopolis c099dfe760 Throwing in the towel on eigenvectors for now
Merge-Checker / build_and_test (pull_request) Successful in 26s
2025-06-06 16:33:20 -04:00
Cynopolis d84664b567 Improved on old unit tests
Merge-Checker / build_and_test (pull_request) Failing after 22s
2025-06-05 15:10:00 -04:00
Cynopolis 1091bbda32 Got QR decomposition fully working! (The unit tests were wrong)
Merge-Checker / build_and_test (pull_request) Successful in 37s
2025-06-03 10:01:52 -04:00
Cynopolis bec70facb2 Fixed clangd type hints in the matrix.cpp file
Merge-Checker / build_and_test (pull_request) Failing after 26s
2025-06-03 09:08:23 -04:00
Cynopolis 75edad3d0a Made my own equally wrong QR factorization
Merge-Checker / build_and_test (pull_request) Failing after 20s
2025-06-02 21:44:41 -04:00
Cynopolis 64820553c7 New norms and division by scalar
Merge-Checker / build_and_test (pull_request) Failing after 21s
2025-06-02 16:19:23 -04:00
Cynopolis 60a2b12b5f Replaced normalize with EuclideanNorm
Merge-Checker / build_and_test (pull_request) Failing after 21s
2025-06-02 14:26:41 -04:00
Cynopolis 37556c7c81 Made unit tests a little better and fixed matrix multiplication errors for non-square amtrices
Merge-Checker / build_and_test (pull_request) Failing after 20s
2025-06-02 10:49:16 -04:00
Cynopolis 6fdab5be30 Added unit tests for eigen
Merge-Checker / build_and_test (pull_request) Successful in 23s
2025-05-30 15:26:19 -04:00
Cynopolis d07ac43f7b Added function comments
Merge-Checker / build_and_test (pull_request) Successful in 23s
2025-05-30 14:47:42 -04:00
Cynopolis 74afbfeab8 Added QR decomposition functions
Merge-Checker / build_and_test (pull_request) Successful in 24s
2025-05-30 09:07:26 -04:00
Cynopolis 1715d2b46c Merge pull request 'Add a merge checker script' (#1) from Testing-merge-checker into main
Reviewed-on: #1
2025-05-29 20:36:30 +00:00
Cynopolis 296f233b28 Updated README
Merge-Checker / build_and_test (pull_request) Successful in 23s
2025-05-29 16:35:52 -04:00
Cynopolis 32c2a5cef2 Added a check to see if the timing results have signifigantly changed
Update matrix-timing-tests timings [skip ci]

Fixing timing test runner

Update matrix-timing-tests timings

Removed the seperate benchmark action
2025-05-29 16:34:31 -04:00
Cynopolis 54d9699df8 Added a merge checker script that has to run before you can merge to main
Updated merge checker and seperated the matrix tests fro mthe timing tests

Added matrix test timings

Timings get auto-comitted

Update matrix-timing-tests timings [skip ci]

Updated readme

Update matrix-timing-tests timings [skip ci]

Fixing auto-checkout issues

updated readme

Update matrix-timing-tests timings [skip ci]

Split timing tests into its own job

Update matrix-timing-tests timings [skip ci]
2025-05-29 16:34:28 -04:00
Cynopolis 8a15459fc8 Adding a merge checker script 2025-05-21 17:46:49 -04:00
Cynopolis dee19b54ad Added a ToEulerAngle function 2025-04-09 18:41:45 -04:00
Cynopolis 4b802458ef Changed matrix unit tests to reflect new syntax 2025-02-09 20:53:37 -05:00
Cynopolis e92fc6e5a0 Made the dot product public 2025-02-09 19:03:32 -05:00
Cynopolis b21236e5db Fixed quaternion equals operator 2025-02-09 11:32:46 -05:00
Cynopolis b897b13880 Merge branch 'main' of https://github.com/Cynopolis/Vector3D 2025-02-09 11:18:03 -05:00
Cynopolis 1a0af95fe7 Reworked how getting a submatrix works 2025-02-09 11:17:45 -05:00
Cynopolis c8dce7d7d8 Fixed broken unit tests 2025-02-09 00:17:16 -05:00
Cynopolis f51afb42e0 Refactored file layout because platformio failed to find things 2025-02-08 23:57:43 -05:00
Cynopolis 2385446ac5 Reworked some of the matrix interface 2025-02-08 18:06:43 -05:00
Cynopolis 713809a82b Fixed library json file so platformio can build again 2025-02-08 00:08:36 -05:00
Cynopolis 55ff4aa693 Finished implimenting quaternion basics 2025-02-07 23:40:56 -05:00
Cynopolis aa8056240a Fixed quaternion multiplication 2025-02-07 19:19:06 -05:00
Cynopolis 28c30c5ea7 Moved unit tests into their respective module's subfolders 2025-02-06 23:45:15 -05:00
Cynopolis 742749457c Implimented the quaternion class and added unit tests 2025-02-06 23:44:55 -05:00
Cynopolis 6e480dce86 Added quaternion class 2025-02-06 23:16:51 -05:00
Cynopolis 39274eb964 Refactored the src dir layout 2025-02-06 22:02:50 -05:00
Cynopolis 3b023d2104 Multiplication was completely broken actually 2025-02-06 21:56:54 -05:00
Cynopolis 9726ebbca0 Fixed typo and missing include 2025-02-04 22:06:34 -05:00
Cynopolis ab2d9f002b Added scalar addition / subtraction 2025-02-04 14:33:29 -05:00
Cynopolis 437d209200 Added a scalar divisor operator 2025-02-03 15:21:22 -05:00
Cynopolis cccadc5d21 consted some vector functions 2025-02-03 12:46:26 -05:00
Cynopolis 519c953fcb Added inline to explicit template specialization functions 2025-02-03 12:34:37 -05:00
Cynopolis c1a1f994ea Fixed some cmake errors 2025-02-03 12:23:13 -05:00
Cynopolis fee5486ea2 Refactored folder layout 2025-02-03 12:20:30 -05:00
Cynopolis 4a25414b92 Got vector unit tests compiling 2025-02-03 11:59:49 -05:00
Cynopolis ae4806510b Added better support for casting vector3d to/from MAtrix 2025-02-03 10:10:31 -05:00
31 changed files with 4318 additions and 379 deletions
Binary file not shown.
+102
View File
@@ -0,0 +1,102 @@
name: Merge-Checker
on:
pull_request:
branches: ["**"]
jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v3
with:
persist-credentials: true
fetch-depth: 0
- name: Install dependencies (CMake + Ninja + build tools)
run: |
sudo apt-get update
sudo apt-get install -y cmake ninja-build build-essential time git
- name: Configure project with CMake
run: cmake -G Ninja -S . -B build/
- name: Build with Ninja
run: ninja -C build/
- name: Run all unit tests except matrix-timing-tests
run: |
for test_exec in build/unit-tests/matrix-tests build/unit-tests/quaternion-tests build/unit-tests/vector-3d-tests; do
if [ -x "$test_exec" ]; then
echo "Running $test_exec"
"$test_exec"
else
echo "Warning: $test_exec not found or not executable"
fi
done
- name: Run matrix-timing-tests
run: |
mkdir -p unit-tests/timing-results
if [ -x build/unit-tests/matrix-timing-tests ]; then
echo "Running matrix-timing-tests with timing"
/usr/bin/time -v build/unit-tests/matrix-timing-tests -d yes &> unit-tests/timing-results/matrix-timing-tests.txt
cat unit-tests/timing-results/matrix-timing-tests.txt
else
echo "matrix-timing-tests executable not found or not executable"
exit 1
fi
- name: Compare timing results
id: check_diff
run: |
git show origin/${{ github.event.pull_request.head.ref }}:unit-tests/timing-results/matrix-timing-tests.txt > old.txt || echo "" > old.txt
cp unit-tests/timing-results/matrix-timing-tests.txt new.txt
echo "Comparing timing results for changes ≥ 0.1s (ignoring 'Timing Tests' lines)..."
changed=0
awk -v changed_ref=/tmp/timings_changed.flag '
BEGIN {
change_threshold = 0.1
}
FILENAME == "old.txt" && /^[0-9]+\.[0-9]+ s: / {
label = substr($0, index($0, ":") + 2)
if (label != "Timing Tests") {
label_times[label] = $1
}
}
FILENAME == "new.txt" && /^[0-9]+\.[0-9]+ s: / {
new_time = $1
label = substr($0, index($0, ":") + 2)
if (label == "Timing Tests") next
old_time = label_times[label]
delta = new_time - old_time
if (delta < 0) delta = -delta
if (old_time != "" && delta >= change_threshold) {
printf "⚠️ %.3f s → %.3f s: %s (Δ=%.3f s)\n", old_time, new_time, label, delta
system("touch " changed_ref)
} else if (old_time == "") {
printf "🆕 New timing entry: %.3f s: %s\n", new_time, label
system("touch " changed_ref)
}
}
END {
if (!system("test -f " changed_ref)) {
exit 0
} else {
print "✅ Timings havent changed significantly (Δ < 0.1s)."
exit 0
}
}
' old.txt new.txt
if [ -f /tmp/timings_changed.flag ]; then
echo "timings_changed=true" >> $GITHUB_OUTPUT
else
echo "timings_changed=false" >> $GITHUB_OUTPUT
fi
+1
View File
@@ -1 +1,2 @@
build/
.cache/
+13 -4
View File
@@ -27,16 +27,25 @@
"internalConsoleOptions": "openOnSessionStart"
},
{
"name": "Run Matrix Unit Tests",
"type": "cpp",
"name": "Debug Quaternion Unit Tests",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/unit-tests/matrix-tests",
"program": "${workspaceFolder}/build/unit-tests/quaternion-tests",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"preLaunchTask": "build_tests", // Compile unit tests before running
"MIMode": "gdb",
"miDebuggerPath": "/usr/bin/gdb", // Adjust to your debugger path
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
],
"preLaunchTask": "build_tests", // Task to compile unit tests
"internalConsoleOptions": "openOnSessionStart"
}
]
+12 -6
View File
@@ -1,8 +1,5 @@
{
"C_Cpp.intelliSenseEngine": "default",
"clangd.arguments": [
"--include-directory=build/unit-tests"
],
"C_Cpp.default.intelliSenseMode": "linux-gcc-x64",
"files.associations": {
"*.h": "cpp",
@@ -70,8 +67,17 @@
"thread": "cpp",
"typeinfo": "cpp",
"variant": "cpp",
"shared_mutex": "cpp"
"shared_mutex": "cpp",
"charconv": "cpp",
"format": "cpp",
"csignal": "cpp",
"span": "cpp"
},
"clangd.enable": false,
"C_Cpp.dimInactiveRegions": false
"clangd.enable": true,
"C_Cpp.dimInactiveRegions": false,
"editor.defaultFormatter": "xaver.clang-format",
"clangd.inactiveRegions.useBackgroundHighlight": false,
"clangd.arguments": [
"--compile-commands-dir=${workspaceFolder}/build"
],
}
+4 -2
View File
@@ -4,12 +4,14 @@
{
"label": "build_tests",
"type": "shell",
"command": "cd build && ninja matrix-tests",
"command": "cd build && ninja",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": ["$gcc"],
"problemMatcher": [
"$gcc"
],
"detail": "Generated task to build unit test executable"
}
]
+13 -32
View File
@@ -1,40 +1,21 @@
cmake_minimum_required(VERSION 3.6)
cmake_minimum_required (VERSION 3.11)
project(Vector3D)
add_subdirectory(src)
add_subdirectory(unit-tests)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
add_compile_options(-fdiagnostics-color=always)
add_compile_options(-Wall -Wextra -Wpedantic)
add_compile_options (-fdiagnostics-color=always)
set(CMAKE_COLOR_DIAGNOSTICS ON)
# Vector3d
add_library(Vector3D
STATIC
Vector3D.hpp
include(FetchContent)
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.8.0 # or a later release
)
set_target_properties(Vector3D
PROPERTIES
LINKER_LANGUAGE CXX
)
target_include_directories(Vector3D PUBLIC
include
)
# Matrix
add_library(Matrix
STATIC
Matrix.hpp
Matrix.cpp
)
set_target_properties(Matrix
PROPERTIES
LINKER_LANGUAGE CXX
)
target_include_directories(Matrix
PUBLIC
.
)
FetchContent_MakeAvailable(Catch2)
+12 -1
View File
@@ -1 +1,12 @@
A Simple matrix math library focused on embedded development which avoids and heap memory allocation unless you explicitly ask for it.
# Introduction
This matrix math library is focused on embedded development and avoids any heap memory allocation unless you explicitly ask for it.
It uses templates to pre-allocate matrices on the stack.
# Building
1. Initialize the repositiory with the command:
```bash
cmake -S . -B build -G Ninja
```
2. Go into the build folder and run `ninja`
3. That's it. You can test out the build by running `./unit-tests/matrix-tests`
-81
View File
@@ -1,81 +0,0 @@
#pragma once
#include <cstdint>
#include <cmath>
#include <type_traits>
template <typename Type>
class V3D{
public:
constexpr V3D(const V3D& other):
x(other.x),
y(other.y),
z(other.z){
static_assert(std::is_arithmetic<Type>::value, "Type must be a number");
}
constexpr V3D(Type x=0, Type y=0, Type z=0):
x(x),
y(y),
z(z){
static_assert(std::is_arithmetic<Type>::value, "Type must be a number");
}
template <typename OtherType>
constexpr V3D(const V3D<OtherType> other):
x(static_cast<Type>(other.x)),
y(static_cast<Type>(other.y)),
z(static_cast<Type>(other.z)){
static_assert(std::is_arithmetic<Type>::value, "Type must be a number");
static_assert(std::is_arithmetic<OtherType>::value, "OtherType must be a number");
}
V3D& operator=(const V3D &other){
this->x = other.x;
this->y = other.y;
this->z = other.z;
return *this;
}
V3D& operator+=(const V3D &other){
this->x += other.x;
this->y += other.y;
this->z += other.z;
return *this;
}
V3D& operator-=(const V3D &other){
this->x -= other.x;
this->y -= other.y;
this->z -= other.z;
return *this;
}
V3D& operator/=(const Type scalar){
if(scalar == 0){
return *this;
}
this->x /= scalar;
this->y /= scalar;
this->z /= scalar;
return *this;
}
V3D& operator*=(const Type scalar){
this->x *= scalar;
this->y *= scalar;
this->z *= scalar;
return *this;
}
bool operator==(const V3D &other){
return this->x == other.x && this->y == other.y && this->z == other.z;
}
float magnitude(){
return std::sqrt(static_cast<float>(this->x * this->x + this->y * this->y + this->z * this->z));
}
Type x;
Type y;
Type z;
};
+20
View File
@@ -0,0 +1,20 @@
{
"name": "Vector3D",
"version": "1.0.0",
"description": "Contains a V3D object for easy 3d vector math and a Matrix object for more complicated linear algebra operations.",
"keywords": "linear algebra, vector, matrix, 3D",
"repository": {
"type": "git",
"url": "https://github.com/Cynopolis/Vector3D.git"
},
"authors": [
{
"name": "Cynopolis",
"email": "megaveganzombie@gmail.com",
"url": "https://github.com/Cynopolis"
}
],
"license": "None Yet",
"frameworks": "*",
"platforms": "*"
}
+76
View File
@@ -0,0 +1,76 @@
# Quaternion Interface
add_library(vector-3d-intf
INTERFACE
)
target_include_directories(vector-3d-intf
INTERFACE
.
)
target_link_libraries(vector-3d-intf
INTERFACE
)
# Quaternion
add_library(quaternion
STATIC
Quaternion.cpp
)
target_link_libraries(quaternion
PUBLIC
vector-3d-intf
PRIVATE
)
set_target_properties(quaternion
PROPERTIES
LINKER_LANGUAGE CXX
)
# Vector3d
add_library(vector-3d
STATIC
Vector3D.cpp
)
target_link_libraries(vector-3d
PUBLIC
vector-3d-intf
PRIVATE
)
# Matrix
add_library(matrix
STATIC
Matrix.cpp
)
target_link_libraries(matrix
PUBLIC
vector-3d-intf
PRIVATE
)
set_target_properties(matrix
PROPERTIES
LINKER_LANGUAGE CXX
)
# SVD
add_library(svd
STATIC
SVD.cpp
)
target_link_libraries(svd
PUBLIC
vector-3d-intf
PRIVATE
)
set_target_properties(svd
PROPERTIES
LINKER_LANGUAGE CXX
)
+215 -51
View File
@@ -1,3 +1,10 @@
// This #ifndef section makes clangd happy so that it can properly do type hints
// in this file
#ifndef MATRIX_H_
#define MATRIX_H_
#include "Matrix.hpp"
#endif
#ifdef MATRIX_H_ // since the .cpp file has to be included by the .hpp file this
// will evaluate to true
#include "Matrix.hpp"
@@ -5,12 +12,7 @@
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <type_traits>
template <uint8_t rows, uint8_t columns>
Matrix<rows, columns>::Matrix(float value) {
this->Fill(value);
}
#include <cstring>
template <uint8_t rows, uint8_t columns>
Matrix<rows, columns>::Matrix(const std::array<float, rows * columns> &array) {
@@ -18,18 +20,37 @@ Matrix<rows, columns>::Matrix(const std::array<float, rows * columns> &array) {
}
template <uint8_t rows, uint8_t columns>
template <typename... Args>
template <typename... Args,
std::enable_if_t<(std::is_arithmetic_v<Args> && ...), int>>
Matrix<rows, columns>::Matrix(Args... args) {
constexpr uint16_t arraySize{static_cast<uint16_t>(rows) *
static_cast<uint16_t>(columns)};
std::initializer_list<float> initList{static_cast<float>(args)...};
// if there is only one value, we actually want to do a fill
if (sizeof...(args) == 1) {
this->Fill(*initList.begin());
}
static_assert(sizeof...(args) == arraySize || sizeof...(args) == 1,
"You did not provide the right amount of initializers for this "
"matrix size");
// choose whichever buffer size is smaller for the copy length
uint32_t minSize =
std::min(arraySize, static_cast<uint16_t>(initList.size()));
memcpy(this->matrix.begin(), initList.begin(), minSize * sizeof(float));
}
template <uint8_t rows, uint8_t columns>
Matrix<rows, columns> Matrix<rows, columns>::Identity() {
Matrix<rows, columns> identityMatrix{0};
uint32_t minDimension = std::min(rows, columns);
for (uint8_t idx{0}; idx < minDimension; idx++) {
identityMatrix[idx][idx] = 1;
}
return identityMatrix;
}
template <uint8_t rows, uint8_t columns>
Matrix<rows, columns>::Matrix(const Matrix<rows, columns> &other) {
for (uint8_t row_idx{0}; row_idx < rows; row_idx++) {
@@ -86,26 +107,23 @@ Matrix<rows, columns>::Sub(const Matrix<rows, columns> &other,
template <uint8_t rows, uint8_t columns>
template <uint8_t other_columns>
Matrix<rows, columns> &
Matrix<rows, other_columns> &
Matrix<rows, columns>::Mult(const Matrix<columns, other_columns> &other,
Matrix<rows, other_columns> &result) const {
// allocate some buffers for all of our dot products
Matrix<1, columns> this_row;
Matrix<rows, 1> other_column;
Matrix<1, rows> other_column_t;
Matrix<columns, 1> other_column;
for (uint8_t row_idx{0}; row_idx < rows; row_idx++) {
// get our row
this->GetRow(row_idx, this_row);
for (uint8_t column_idx{0}; column_idx < columns; column_idx++) {
for (uint8_t column_idx{0}; column_idx < other_columns; column_idx++) {
// get the other matrix'ss column
other.GetColumn(column_idx, other_column);
// transpose the other matrix's column
other_column.Transpose(other_column_t);
// the result's index is equal to the dot product of these two vectors
result[row_idx][column_idx] =
Matrix<rows, columns>::dotProduct(this_row, other_column_t);
Matrix<rows, columns>::DotProduct(this_row, other_column.Transpose());
}
}
@@ -125,13 +143,13 @@ Matrix<rows, columns>::Mult(float scalar, Matrix<rows, columns> &result) const {
}
template <uint8_t rows, uint8_t columns>
Matrix<rows, columns> &
Matrix<rows, columns>::Invert(Matrix<rows, columns> &result) const {
Matrix<rows, columns> Matrix<rows, columns>::Invert() const {
// since all matrix sizes have to be statically specified at compile time we
// can do this
static_assert(rows == columns,
"Your matrix isn't square and can't be inverted");
Matrix<rows, columns> result{};
// unfortunately we can't calculate this at compile time so we'll just reurn
// zeros
float determinant{this->Det()};
@@ -160,8 +178,8 @@ Matrix<rows, columns>::Invert(Matrix<rows, columns> &result) const {
}
template <uint8_t rows, uint8_t columns>
Matrix<columns, rows> &
Matrix<rows, columns>::Transpose(Matrix<columns, rows> &result) const {
Matrix<columns, rows> Matrix<rows, columns>::Transpose() const {
Matrix<columns, rows> result{};
for (uint8_t column_idx{0}; column_idx < rows; column_idx++) {
for (uint8_t row_idx{0}; row_idx < columns; row_idx++) {
result[row_idx][column_idx] = this->Get(column_idx, row_idx);
@@ -173,9 +191,10 @@ Matrix<rows, columns>::Transpose(Matrix<columns, rows> &result) const {
// explicitly define the determinant for a 2x2 matrix because it is definitely
// the fastest way to calculate a 2x2 matrix determinant
template <> float Matrix<0, 0>::Det() const { return 1e+6; }
template <> float Matrix<1, 1>::Det() const { return this->matrix[0]; }
template <> float Matrix<2, 2>::Det() const {
// template <>
// inline float Matrix<0, 0>::Det() const { return 1e+6; }
template <> inline float Matrix<1, 1>::Det() const { return this->matrix[0]; }
template <> inline float Matrix<2, 2>::Det() const {
return this->matrix[0] * this->matrix[3] - this->matrix[1] * this->matrix[2];
}
@@ -271,8 +290,13 @@ void Matrix<rows, columns>::ToString(std::string &stringBuffer) const {
}
template <uint8_t rows, uint8_t columns>
std::array<float, columns> &Matrix<rows, columns>::
operator[](uint8_t row_index) {
const float *Matrix<rows, columns>::ToArray() const {
return this->matrix.data();
}
template <uint8_t rows, uint8_t columns>
std::array<float, columns> &
Matrix<rows, columns>::operator[](uint8_t row_index) {
if (row_index > rows - 1) {
// TODO: We should throw something here instead of failing quietly.
row_index = 0;
@@ -284,38 +308,36 @@ operator[](uint8_t row_index) {
}
template <uint8_t rows, uint8_t columns>
Matrix<rows, columns> &Matrix<rows, columns>::
operator=(const Matrix<rows, columns> &other) {
for (uint8_t row_idx{0}; row_idx < rows; row_idx++) {
for (uint8_t column_idx{0}; column_idx < columns; column_idx++) {
this->matrix[row_idx * columns + column_idx] =
other.Get(row_idx, column_idx);
}
}
Matrix<rows, columns> &
Matrix<rows, columns>::operator=(const Matrix<rows, columns> &other) {
memcpy(this->matrix.begin(), other.matrix.begin(),
rows * columns * sizeof(float));
// return a reference to ourselves so you can chain together these functions
return *this;
}
template <uint8_t rows, uint8_t columns>
Matrix<rows, columns> Matrix<rows, columns>::
operator+(const Matrix<rows, columns> &other) const {
Matrix<rows, columns>
Matrix<rows, columns>::operator+(const Matrix<rows, columns> &other) const {
Matrix<rows, columns> buffer{};
this->Add(other, buffer);
return buffer;
}
template <uint8_t rows, uint8_t columns>
Matrix<rows, columns> Matrix<rows, columns>::
operator-(const Matrix<rows, columns> &other) const {
Matrix<rows, columns>
Matrix<rows, columns>::operator-(const Matrix<rows, columns> &other) const {
Matrix<rows, columns> buffer{};
this->Sub(other, buffer);
return buffer;
}
template <uint8_t rows, uint8_t columns>
Matrix<rows, columns> Matrix<rows, columns>::
operator*(const Matrix<rows, columns> &other) const {
Matrix<rows, columns> buffer{};
template <uint8_t other_columns>
Matrix<rows, other_columns> Matrix<rows, columns>::operator*(
const Matrix<columns, other_columns> &other) const {
Matrix<rows, other_columns> buffer{};
this->Mult(other, buffer);
return buffer;
}
@@ -327,9 +349,25 @@ Matrix<rows, columns> Matrix<rows, columns>::operator*(float scalar) const {
return buffer;
}
template <uint8_t rows, uint8_t columns>
Matrix<rows, columns> Matrix<rows, columns>::operator/(float scalar) const {
Matrix<rows, columns> buffer = *this;
if (scalar == 0) {
buffer.Fill(1e+10);
return buffer;
}
for (uint8_t row = 0; row < rows; row++) {
for (uint8_t column = 0; column < columns; column++) {
buffer[row][column] /= scalar;
}
}
return buffer;
}
template <uint8_t rows, uint8_t columns>
template <uint8_t vector_size>
float Matrix<rows, columns>::dotProduct(const Matrix<1, vector_size> &vec1,
float Matrix<rows, columns>::DotProduct(const Matrix<1, vector_size> &vec1,
const Matrix<1, vector_size> &vec2) {
float sum{0};
for (uint8_t i{0}; i < vector_size; i++) {
@@ -341,7 +379,7 @@ float Matrix<rows, columns>::dotProduct(const Matrix<1, vector_size> &vec1,
template <uint8_t rows, uint8_t columns>
template <uint8_t vector_size>
float Matrix<rows, columns>::dotProduct(const Matrix<vector_size, 1> &vec1,
float Matrix<rows, columns>::DotProduct(const Matrix<vector_size, 1> &vec1,
const Matrix<vector_size, 1> &vec2) {
float sum{0};
for (uint8_t i{0}; i < vector_size; i++) {
@@ -413,8 +451,8 @@ Matrix<rows, columns>::adjugate(Matrix<rows, columns> &result) const {
}
template <uint8_t rows, uint8_t columns>
Matrix<rows, columns> &
Matrix<rows, columns>::Normalize(Matrix<rows, columns> &result) const {
float Matrix<rows, columns>::EuclideanNorm() const {
float sum{0};
for (uint8_t row_idx{0}; row_idx < rows; row_idx++) {
for (uint8_t column_idx{0}; column_idx < columns; column_idx++) {
@@ -423,21 +461,147 @@ Matrix<rows, columns>::Normalize(Matrix<rows, columns> &result) const {
}
}
if (sum == 0) {
// this wouldn't do anything anyways
result.Fill(1e+6);
return result;
return sqrt(sum);
}
template <uint8_t rows, uint8_t columns>
template <uint8_t sub_rows, uint8_t sub_columns, uint8_t row_offset,
uint8_t column_offset>
Matrix<sub_rows, sub_columns> Matrix<rows, columns>::SubMatrix() const {
// static assert that sub_rows + row_offset <= rows
// static assert that sub_columns + column_offset <= columns
static_assert(sub_rows + row_offset <= rows,
"The submatrix you're trying to get is out of bounds (rows)");
static_assert(
sub_columns + column_offset <= columns,
"The submatrix you're trying to get is out of bounds (columns)");
Matrix<sub_rows, sub_columns> buffer{};
for (uint8_t row_idx{0}; row_idx < sub_rows; row_idx++) {
for (uint8_t column_idx{0}; column_idx < sub_columns; column_idx++) {
buffer[row_idx][column_idx] =
this->Get(row_idx + row_offset, column_idx + column_offset);
}
}
return buffer;
}
template <uint8_t rows, uint8_t columns>
template <uint8_t sub_rows, uint8_t sub_columns>
void Matrix<rows, columns>::SetSubMatrix(
uint8_t rowOffset, uint8_t columnOffset,
const Matrix<sub_rows, sub_columns> &sub_matrix) {
int16_t adjustedSubRows = sub_rows;
int16_t adjustedSubColumns = sub_columns;
int16_t adjustedRowOffset = rowOffset;
int16_t adjustedColumnOffset = columnOffset;
// a bunch of safety checks to make sure we don't overflow the matrix
if (sub_rows > rows) {
adjustedSubRows = rows;
}
if (sub_columns > columns) {
adjustedSubColumns = columns;
}
sum = sqrt(sum);
if (adjustedSubRows + adjustedRowOffset >= rows) {
adjustedRowOffset =
std::max(0, static_cast<int16_t>(rows) - adjustedSubRows);
}
for (uint8_t row_idx{0}; row_idx < rows; row_idx++) {
for (uint8_t column_idx{0}; column_idx < columns; column_idx++) {
result[row_idx][column_idx] = this->Get(row_idx, column_idx) / sum;
if (adjustedSubColumns + adjustedColumnOffset >= columns) {
adjustedColumnOffset =
std::max(0, static_cast<int16_t>(columns) - adjustedSubColumns);
}
for (uint8_t row_idx{0}; row_idx < adjustedSubRows; row_idx++) {
for (uint8_t column_idx{0}; column_idx < adjustedSubColumns; column_idx++) {
this->matrix[(row_idx + adjustedRowOffset) * columns + column_idx +
adjustedColumnOffset] = sub_matrix.Get(row_idx, column_idx);
}
}
}
// QR decomposition: decomposes this matrix A into Q and R
// Assumes square matrix
template <uint8_t rows, uint8_t columns>
void Matrix<rows, columns>::QRDecomposition(Matrix<rows, columns> &Q,
Matrix<columns, columns> &R) const {
static_assert(columns <= rows, "QR decomposition requires columns <= rows");
Q.Fill(0);
R.Fill(0);
Matrix<rows, 1> a_col, e, u, Q_column_k{};
Matrix<1, rows> e_T{};
for (uint8_t column = 0; column < columns; column++) {
this->GetColumn(column, a_col);
u = a_col;
// -----------------------
// ----- CALCULATE Q -----
// -----------------------
for (uint8_t k = 0; k <= column; k++) {
Q.GetColumn(k, Q_column_k);
Matrix<1, rows> Q_column_k_T = Q_column_k.Transpose();
u = u - Q_column_k * (Q_column_k_T * a_col);
}
float norm = u.EuclideanNorm();
if (norm > 1e-4) {
u = u / norm;
} else {
u.Fill(0);
}
Q.SetSubMatrix(0, column, u);
// -----------------------
// ----- CALCULATE R -----
// -----------------------
for (uint8_t k = 0; k <= column; k++) {
Q.GetColumn(k, e);
R[k][column] = (a_col.Transpose() * e).Get(0, 0);
}
}
}
template <uint8_t rows, uint8_t columns>
void Matrix<rows, columns>::EigenQR(Matrix<rows, rows> &eigenVectors,
Matrix<rows, 1> &eigenValues,
uint32_t maxIterations,
float tolerance) const {
static_assert(rows > 1, "Matrix size must be > 1 for QR iteration");
static_assert(rows == columns, "Matrix size must be square for QR iteration");
Matrix<rows, rows> Ak = *this; // Copy original matrix
Matrix<rows, rows> QQ{Matrix<rows, rows>::Identity()};
Matrix<rows, rows> shift{0};
for (uint32_t iter = 0; iter < maxIterations; ++iter) {
Matrix<rows, rows> Q, R;
// // QR shift lets us "attack" the first diagonal to speed up the algorithm
// shift = Matrix<rows, rows>::Identity() * Ak[rows - 1][rows - 1];
(Ak - shift).QRDecomposition(Q, R);
Ak = R * Q + shift;
QQ = QQ * Q;
// Check convergence: off-diagonal norm
float offDiagSum = 0.0f;
for (uint32_t row = 1; row < rows; row++) {
for (uint32_t column = 0; column < row; column++) {
offDiagSum += fabs(Ak[row][column]);
}
}
return result;
if (offDiagSum < tolerance) {
break;
}
}
// Diagonal elements are the eigenvalues
for (uint8_t i = 0; i < rows; i++) {
eigenValues[i][0] = Ak[i][i];
}
eigenVectors = QQ;
}
#endif // MATRIX_H_
+76 -21
View File
@@ -1,8 +1,9 @@
#ifndef MATRIX_H_
#define MATRIX_H_
#pragma once
#include <array>
#include <cstdint>
#include <string>
#include <type_traits>
// TODO: Add a function to calculate eigenvalues/vectors
// TODO: Add a function to compute RREF
@@ -11,16 +12,13 @@
template <uint8_t rows, uint8_t columns> class Matrix {
public:
static_assert(rows > 0, "Template error: rows must be greater than 0.");
static_assert(columns > 0, "Template error: columns must be greater than 0.");
/**
* @brief create a matrix but leave all of its values unitialized
*/
Matrix() = default;
/**
* @brief Create a matrix but fill all of its entries with one value
*/
Matrix(float value);
/**
* @brief Initialize a matrix with an array
*/
@@ -32,9 +30,18 @@ public:
Matrix(const Matrix<rows, columns> &other);
/**
* @brief Initialize a matrix directly with any number of arguments
* @brief Initialize a matrix directly with scalar values
* Uses SFINAE to only accept arithmetic types (int, float, double, etc.)
*/
template <typename... Args> Matrix(Args... args);
template <typename... Args,
std::enable_if_t<(std::is_arithmetic_v<Args> && ...), int> = 0>
Matrix(Args... args);
/**
* @brief Create an identity matrix
*/
static Matrix<rows, columns> Identity();
/**
* @brief Set all elements in this to value
*/
@@ -64,7 +71,7 @@ public:
* @param result A buffer to store the result into
*/
template <uint8_t other_columns>
Matrix<rows, columns> &Mult(const Matrix<columns, other_columns> &other,
Matrix<rows, other_columns> &Mult(const Matrix<columns, other_columns> &other,
Matrix<rows, other_columns> &result) const;
/**
@@ -111,19 +118,20 @@ public:
* @param result A buffer to store the result into
* @warning this is super slow! Only call it if you absolutely have to!!!
*/
Matrix<rows, columns> &Invert(Matrix<rows, columns> &result) const;
Matrix<rows, columns> Invert() const;
/**
* @brief Transpose this matrix
* @param result A buffer to store the result into
*/
Matrix<columns, rows> &Transpose(Matrix<columns, rows> &result) const;
Matrix<columns, rows> Transpose() const;
/**
* @brief reduce the matrix so the sum of its elements equal 1
* @brief Returns the euclidean magnitude of the matrix. Also known as the L2
* norm
* @param result a buffer to store the result into
*/
Matrix<rows, columns> &Normalize(Matrix<rows, columns> &result) const;
float EuclideanNorm() const;
/**
* @brief Get a row from the matrix
@@ -150,8 +158,16 @@ public:
*/
constexpr uint8_t GetColumnSize() { return columns; }
/**
* @brief Write a string representation of the matrix into the buffer
*/
void ToString(std::string &stringBuffer) const;
/**
* @brief Returns the internal representation of the matrix as an array
*/
const float *ToArray() const;
/**
* @brief Get an element from the matrix
* @param row the row index of the element
@@ -178,29 +194,68 @@ public:
Matrix<rows, columns> operator-(const Matrix<rows, columns> &other) const;
Matrix<rows, columns> operator*(const Matrix<rows, columns> &other) const;
template <uint8_t other_columns>
Matrix<rows, other_columns>
operator*(const Matrix<columns, other_columns> &other) const;
Matrix<rows, columns> operator*(float scalar) const;
private:
Matrix<rows, columns> operator/(float scalar) const;
template <uint8_t sub_rows, uint8_t sub_columns, uint8_t row_offset,
uint8_t column_offset>
Matrix<sub_rows, sub_columns> SubMatrix() const;
template <uint8_t sub_rows, uint8_t sub_columns>
void SetSubMatrix(uint8_t rowOffset, uint8_t columnOffset,
const Matrix<sub_rows, sub_columns> &sub_matrix);
/**
* @brief take the dot product of the two vectors
*/
template <uint8_t vector_size>
static float dotProduct(const Matrix<1, vector_size> &vec1,
static float DotProduct(const Matrix<1, vector_size> &vec1,
const Matrix<1, vector_size> &vec2);
template <uint8_t vector_size>
static float dotProduct(const Matrix<vector_size, 1> &vec1,
static float DotProduct(const Matrix<vector_size, 1> &vec1,
const Matrix<vector_size, 1> &vec2);
static float DotProduct(const Matrix<1, 1> &vec1, const Matrix<1, 1> &vec2) {
return vec1.Get(0, 0) * vec2.Get(0, 0);
}
/**
* @brief Performs QR decomposition on this matrix
* @param Q a buffer that will contain Q after the function completes
* @param R a buffer that will contain R after the function completes
*/
void QRDecomposition(Matrix<rows, columns> &Q,
Matrix<columns, columns> &R) const;
/**
* @brief Uses QR decomposition to efficiently calculate the eigenvectors
* and values of this matrix
* @param eigenVectors a buffer that will contain the eigenvectors fo this
* matrix
* @param eigenValues a buffer that will contain the eigenValues fo this
* matrix
* @param maxIterations the number of iterations to perform before giving
* up on reaching the given tolerance
* @param tolerance the level of accuracy to obtain before stopping.
*/
void EigenQR(Matrix<rows, rows> &eigenVectors, Matrix<rows, 1> &eigenValues,
uint32_t maxIterations = 1000, float tolerance = 1e-6f) const;
protected:
std::array<float, rows * columns> matrix;
private:
Matrix<rows, columns> &adjugate(Matrix<rows, columns> &result) const;
void setMatrixToArray(const std::array<float, rows * columns> &array);
std::array<float, rows * columns> matrix;
};
#ifndef MATRIX_H_
#include "Matrix.cpp"
#endif // MATRIX_H_
+120
View File
@@ -0,0 +1,120 @@
#include "Quaternion.h"
#include <cmath>
/**
* @brief Create a quaternion from an angle and axis
* @param angle The angle to rotate by
* @param axis The axis to rotate around
*/
Quaternion Quaternion::FromAngleAndAxis(float angle, const Matrix<1, 3> &axis) {
const float halfAngle = angle / 2;
const float sinHalfAngle = sin(halfAngle);
Matrix<1, 3> normalizedAxis = axis / axis.EuclideanNorm();
return Quaternion{static_cast<float>(cos(halfAngle)),
normalizedAxis.Get(0, 0) * sinHalfAngle,
normalizedAxis.Get(0, 1) * sinHalfAngle,
normalizedAxis.Get(0, 2) * sinHalfAngle};
}
float Quaternion::operator[](uint8_t index) const {
if (index < 4) {
return this->matrix[index];
}
// index out of bounds
return 1e+6;
}
void Quaternion::operator=(const Quaternion &other) {
memcpy(&(this->matrix), &(other.matrix), 4 * sizeof(float));
}
Quaternion Quaternion::operator*(const Quaternion &other) const {
Quaternion result{};
this->Q_Mult(other, result);
return result;
}
Quaternion Quaternion::operator*(float scalar) const {
return Quaternion{this->w * scalar, this->v1 * scalar, this->v2 * scalar,
this->v3 * scalar};
}
Quaternion Quaternion::operator+(const Quaternion &other) const {
return Quaternion{this->w + other.w, this->v1 + other.v1, this->v2 + other.v2,
this->v3 + other.v3};
}
Quaternion &Quaternion::Q_Mult(const Quaternion &other,
Quaternion &buffer) const {
// eq. 6
buffer.w = (other.w * this->w - other.v1 * this->v1 - other.v2 * this->v2 -
other.v3 * this->v3);
buffer.v1 = (other.w * this->v1 + other.v1 * this->w - other.v2 * this->v3 +
other.v3 * this->v2);
buffer.v2 = (other.w * this->v2 + other.v1 * this->v3 + other.v2 * this->w -
other.v3 * this->v1);
buffer.v3 = (other.w * this->v3 - other.v1 * this->v2 + other.v2 * this->v1 +
other.v3 * this->w);
return buffer;
}
Quaternion &Quaternion::Rotate(Quaternion &other, Quaternion &buffer) const {
Quaternion prime{this->w, -this->v1, -this->v2, -this->v3};
buffer.v1 = other.v1;
buffer.v2 = other.v2;
buffer.v3 = other.v3;
buffer.w = 0;
Quaternion temp{};
this->Q_Mult(buffer, temp);
temp.Q_Mult(prime, buffer);
return buffer;
}
void Quaternion::Normalize() {
float magnitude = sqrt(this->v1 * this->v1 + this->v2 * this->v2 +
this->v3 * this->v3 + this->w * this->w);
if (magnitude == 0) {
return;
}
this->v1 /= magnitude;
this->v2 /= magnitude;
this->v3 /= magnitude;
this->w /= magnitude;
}
Matrix<3, 3> Quaternion::ToRotationMatrix() const {
float xx = this->v1 * this->v1;
float yy = this->v2 * this->v2;
float zz = this->v3 * this->v3;
Matrix<3, 3> rotationMatrix{1 - 2 * (yy - zz),
2 * (this->v1 * this->v2 - this->v3 * this->w),
2 * (this->v1 * this->v3 + this->v2 * this->w),
2 * (this->v1 * this->v2 + this->v3 * this->w),
1 - 2 * (xx - zz),
2 * (this->v2 * this->v3 - this->v1 * this->w),
2 * (this->v1 * this->v3 - this->v2 * this->w),
2 * (this->v2 * this->v3 + this->v1 * this->w),
1 - 2 * (xx - yy)};
return rotationMatrix;
};
Matrix<3, 1> Quaternion::ToEulerAngle() const {
float sqv1 = this->v1 * this->v1;
float sqv2 = this->v2 * this->v2;
float sqv3 = this->v3 * this->v3;
float sqw = this->w * this->w;
Matrix<3, 1> eulerAngle;
{
atan2(2.0 * (this->v1 * this->v2 + this->v3 * this->w),
(sqv1 - sqv2 - sqv3 + sqw));
asin(-2.0 * (this->v1 * this->v3 - this->v2 * this->w) /
(sqv1 + sqv2 + sqv3 + sqw));
atan2(2.0 * (this->v2 * this->v3 + this->v1 * this->w),
(-sqv1 - sqv2 + sqv3 + sqw));
};
return eulerAngle;
}
+90
View File
@@ -0,0 +1,90 @@
#ifndef QUATERNION_H_
#define QUATERNION_H_
#include "Matrix.hpp"
class Quaternion : public Matrix<1, 4> {
public:
Quaternion() : Matrix<1, 4>() {}
Quaternion(float w, float v1, float v2, float v3)
: Matrix<1, 4>(w, v1, v2, v3) {}
Quaternion(const Quaternion &q) : Matrix<1, 4>(q.w, q.v1, q.v2, q.v3) {}
Quaternion(const Matrix<1, 4> &matrix) : Matrix<1, 4>(matrix) {}
Quaternion(const std::array<float, 4> &array) : Matrix<1, 4>(array) {}
/**
* @brief Create a quaternion from an angle and axis
* @param angle The angle to rotate by
* @param axis The axis to rotate around
*/
static Quaternion FromAngleAndAxis(float angle, const Matrix<1, 3> &axis);
/**
* @brief Access the elements of the quaternion
* @param index The index of the element to access
* @return The value of the element at the index
*/
float operator[](uint8_t index) const;
/**
* @brief Assign one quaternion to another
*/
void operator=(const Quaternion &other);
/**
* @brief Do quaternion multiplication
*/
Quaternion operator*(const Quaternion &other) const;
/**
* @brief Multiply the quaternion by a scalar
*/
Quaternion operator*(float scalar) const;
/**
* @brief Add two quaternions together
* @param other The quaternion to add to this one
* @return The net quaternion
*/
Quaternion operator+(const Quaternion &other) const;
/**
* @brief Q_Mult a quaternion by another quaternion
* @param other The quaternion to rotate by
* @param buffer The buffer to store the result in
* @return A reference to the buffer
*/
Quaternion &Q_Mult(const Quaternion &other, Quaternion &buffer) const;
/**
* @brief Rotate a quaternion by this quaternion
* @param other The quaternion to rotate
* @param buffer The buffer to store the result in
*
*/
Quaternion &Rotate(Quaternion &other, Quaternion &buffer) const;
/**
* @brief Normalize the quaternion to a magnitude of 1
*/
void Normalize();
/**
* @brief Convert the quaternion to a rotation matrix
* @return The rotation matrix
*/
Matrix<3, 3> ToRotationMatrix() const;
/**
* @brief Convert the quaternion to an Euler angle representation
* @return The Euler angle representation of the quaternion
*/
Matrix<3, 1> ToEulerAngle() const;
// Give people an easy way to access the elements
float &w{matrix[0]};
float &v1{matrix[1]};
float &v2{matrix[2]};
float &v3{matrix[3]};
};
#endif // QUATERNION_H_
+540
View File
@@ -0,0 +1,540 @@
// This #ifndef section makes clangd happy so that it can properly do type hints
// in this file
#ifndef SVD_H_
#define SVD_H_
#include "SVD.hpp"
#endif
#ifdef SVD_H_ // since the .cpp file has to be included by the .hpp file this
// will evaluate to true
#include "SVD.hpp"
#include <cstdint>
// ============================================================================
// SVD Building Block Implementations
// ============================================================================
float SVD::ComputeHouseholder(const float *x, uint8_t len, float *v,
float &alpha) {
// Compute ||x||
float norm = 0.0f;
for (uint8_t i = 0; i < len; i++) {
norm += x[i] * x[i];
}
norm = sqrtf(norm);
if (norm < 1e-30f) {
alpha = 0.0f;
for (uint8_t i = 0; i < len; i++) {
v[i] = 0.0f;
}
return 0.0f;
}
// Choose sign to avoid cancellation: alpha has opposite sign of x[0]
alpha = (x[0] >= 0.0f) ? -norm : norm;
// v = x - alpha * e1, then normalize
float v0 = x[0] - alpha;
// Compute ||v||² directly: v0² + x₁² + ... + xₙ₋₁²
float vv = v0 * v0;
for (uint8_t i = 1; i < len; i++) {
vv += x[i] * x[i];
}
if (vv < 1e-30f) {
// Already aligned with e1
for (uint8_t i = 0; i < len; i++) {
v[i] = (i == 0) ? 1.0f : 0.0f;
}
return norm;
}
float scale = 1.0f / sqrtf(vv);
for (uint8_t i = 0; i < len; i++) {
v[i] = (i == 0) ? v0 * scale : x[i] * scale;
}
return norm;
}
void SVD::ApplyHouseholderLeft(Matrix<5, 5> &W, const float *v,
uint8_t startRow, uint8_t endRow) {
uint8_t len = endRow - startRow + 1;
// Compute vᵀv (should be 2.0 for our normalized vectors, but compute
// explicitly)
float vv = 0.0f;
for (uint8_t i = 0; i < len; i++) {
vv += v[i] * v[i];
}
if (vv < 1e-30f)
return;
float twoOverVv = 2.0f / vv;
// W = (I - 2vvᵀ) · W
for (uint8_t col = 0; col < 5; col++) {
float dot = 0.0f;
for (uint8_t i = 0; i < len; i++) {
dot += v[i] * W[startRow + i][col];
}
dot *= twoOverVv;
for (uint8_t i = 0; i < len; i++) {
W[startRow + i][col] -= dot * v[i];
}
}
}
void SVD::ApplyHouseholderRight(Matrix<5, 5> &W, const float *v,
uint8_t startCol, uint8_t endCol) {
uint8_t len = endCol - startCol + 1;
float vv = 0.0f;
for (uint8_t i = 0; i < len; i++) {
vv += v[i] * v[i];
}
if (vv < 1e-30f)
return;
float twoOverVv = 2.0f / vv;
// W = W · (I - 2vvᵀ)
for (uint8_t row = 0; row < 5; row++) {
float dot = 0.0f;
for (uint8_t i = 0; i < len; i++) {
dot += W[row][startCol + i] * v[i];
}
dot *= twoOverVv;
for (uint8_t i = 0; i < len; i++) {
W[row][startCol + i] -= dot * v[i];
}
}
}
void SVD::ComputeGivens(float x, float y, float &c, float &s) {
float r = sqrtf(x * x + y * y);
if (r < 1e-30f) {
c = 1.0f;
s = 0.0f;
return;
}
c = x / r;
s = y / r;
}
void SVD::ApplyGivensLeft(Matrix<5, 5> &W, uint8_t i, uint8_t j, float c,
float s, uint8_t startCol, uint8_t endCol) {
// [c s] [row_i] = [new_row_i]
// [-s c] [row_j] [new_row_j]
for (uint8_t col = startCol; col <= endCol && col < 5; col++) {
float t1 = W[i][col];
float t2 = W[j][col];
W[i][col] = c * t1 + s * t2;
W[j][col] = -s * t1 + c * t2;
}
}
void SVD::ApplyGivensRight(Matrix<5, 5> &W, uint8_t i, uint8_t j, float c,
float s, uint8_t startRow, uint8_t endRow) {
// [col_i col_j] · [c -s] = [new_col_i new_col_j]
// [s c]
for (uint8_t row = startRow; row <= endRow && row < 5; row++) {
float t1 = W[row][i];
float t2 = W[row][j];
W[row][i] = c * t1 + s * t2;
W[row][j] = -s * t1 + c * t2;
}
}
// ============================================================================
// SVD Implementation - Golub-Kahan-Reinsch Algorithm
// ============================================================================
/**
* @brief SVD for any m×n matrix using Householder bidiagonalization +
* implicit QR iteration on the bidiagonal form.
*
* Given A (m×n), computes U (m×k), Σ (k×k diagonal), Vᵀ (k×n) where
* k = min(m,n) and A = U·Σ·Vᵀ.
*
* We store results as:
* - U: Matrix<m, n> — first k columns are meaningful
* - sigma: Matrix<n, 1> — first k entries are non-zero singular values
* - Vt: Matrix<n, n> — first k rows are meaningful
*
* For m < n (wide matrices), we work with Aᵀ and swap roles of U and V.
*/
template <uint8_t rows, uint8_t columns>
void SVD::SVD(Matrix<rows, columns> &matrixToDecompose,
Matrix<rows, columns> &U, Matrix<columns, 1> &sigma,
Matrix<columns, columns> &Vt) {
static_assert(rows <= 5 && columns <= 5,
"SVD currently supports matrices up to 5×5");
uint8_t m = rows;
uint8_t n = columns;
uint8_t p = (m < n) ? m : n; // rank = min(m,n)
// For wide matrices (m < n), work with Aᵀ instead.
// SVD(A) = U·Σ·Vᵀ ⟺ SVD(Aᵀ) = V·Σ·Uᵀ
// So if we compute SVD(Aᵀ) = Ũ·Σ·Ṽᵀ, then U = Ṽ and Vt = Ũᵀ.
bool transposeNeeded = (m < n);
// Working matrix: always p×p or larger square
Matrix<5, 5> W{0};
for (uint8_t i = 0; i < m; i++) {
for (uint8_t j = 0; j < n; j++) {
float val = matrixToDecompose.Get(i, j);
if (transposeNeeded) {
W[j][i] = val; // store Aᵀ
} else {
W[i][j] = val;
}
}
}
// After bidiagonalization, W holds the bidiagonal matrix B.
// Q_L and Q_R accumulate the Householder transformations.
Matrix<5, 5> QL{0}, QR{0};
for (uint8_t i = 0; i < 5; i++) {
QL[i][i] = 1;
QR[i][i] = 1;
}
// ---- Phase 1: Householder Bidiagonalization ----
// Reduce W to upper bidiagonal form using Householder reflections.
// For a p×q matrix (p ≤ q after transpose), we do p steps:
// Step k: zero out subdiagonal in column k, then superdiagonal in row k
float hhVec[5]; // Householder vector storage
for (uint8_t k = 0; k < p; k++) {
// --- Left Householder on column k, rows k..min(m,n)-1 ---
{
uint8_t len = (m > n) ? m - k : n - k;
if (len <= 1)
continue;
// Extract the column segment
float x[5];
for (uint8_t i = 0; i < len; i++) {
x[i] = W[k + i][k];
}
// Compute Householder reflection: H·x = [α, 0, ..., 0]ᵀ
float norm = 0;
for (uint8_t i = 0; i < len; i++)
norm += x[i] * x[i];
norm = sqrtf(norm);
if (norm < 1e-30f)
continue;
float alpha = (x[0] >= 0) ? -norm : norm;
float v0 = x[0] - alpha;
float vv = v0 * v0 + norm * norm - alpha * x[0];
if (vv < 1e-30f)
continue;
float scale = 1.0f / sqrtf(vv);
// Store Householder vector (first element is implicit 1, rest in hhVec)
hhVec[0] =
v0 * scale; // this is the first element of the reflected vector
for (uint8_t i = 1; i < len; i++) {
hhVec[i] = x[i] * scale;
}
// Apply H from left to W: W = H·W
// For each column j, w[k+i][j] -= 2·v_i·(vᵀ·w_col) / (vᵀv)
float vvNorm = 1.0f + hhVec[0] * hhVec[0];
for (uint8_t i = 1; i < len; i++) {
vvNorm += hhVec[i] * hhVec[i];
}
for (uint8_t j = k; j < n; j++) {
float dot = 0;
for (uint8_t i = 0; i < len; i++) {
dot += hhVec[i] * W[k + i][j];
}
dot *= 2.0f / vvNorm;
for (uint8_t i = 0; i < len; i++) {
W[k + i][j] -= dot * hhVec[i];
}
}
// Apply H from right to QL: QL = QL · H
for (uint8_t j = k; j < m; j++) {
float dot = 0;
for (uint8_t i = 0; i < len; i++) {
dot += hhVec[i] * QL[j][k + i];
}
dot *= 2.0f / vvNorm;
for (uint8_t i = 0; i < len; i++) {
QL[j][k + i] -= dot * hhVec[i];
}
}
}
// --- Right Householder on row k, columns k+1..min(m,n)-1 ---
{
uint8_t len = (p > 1) ? p - 1 - k : 0;
if (len <= 0)
continue;
// Extract the row segment
float x[5];
for (uint8_t i = 0; i < len; i++) {
x[i] = W[k][k + 1 + i];
}
// Compute Householder reflection
float norm = 0;
for (uint8_t i = 0; i < len; i++)
norm += x[i] * x[i];
norm = sqrtf(norm);
if (norm < 1e-30f)
continue;
float alpha = (x[0] >= 0) ? -norm : norm;
float v0 = x[0] - alpha;
float vv = v0 * v0 + norm * norm - alpha * x[0];
if (vv < 1e-30f)
continue;
float scale = 1.0f / sqrtf(vv);
hhVec[0] = v0 * scale;
for (uint8_t i = 1; i < len; i++) {
hhVec[i] = x[i] * scale;
}
// Compute vᵀv
float vvNorm = 1.0f + hhVec[0] * hhVec[0];
for (uint8_t i = 1; i < len; i++) {
vvNorm += hhVec[i] * hhVec[i];
}
// Apply H from right to W: W = W·H
for (uint8_t i = 0; i < m; i++) {
float dot = 0;
for (uint8_t j = 0; j < len; j++) {
dot += hhVec[j] * W[i][k + 1 + j];
}
dot *= 2.0f / vvNorm;
for (uint8_t j = 0; j < len; j++) {
W[i][k + 1 + j] -= dot * hhVec[j];
}
}
// Apply H from right to QR: QR = QR · H
for (uint8_t i = 0; i < n; i++) {
float dot = 0;
for (uint8_t j = 0; j < len; j++) {
dot += hhVec[j] * QR[i][k + 1 + j];
}
dot *= 2.0f / vvNorm;
for (uint8_t j = 0; j < len; j++) {
QR[i][k + 1 + j] -= dot * hhVec[j];
}
}
}
}
// ---- Phase 2: Implicit QR Iteration on Bidiagonal Matrix ----
// W now contains the upper bidiagonal matrix B.
// We apply implicit QR steps to diagonalize it.
uint32_t maxIter = 1000;
float tol = 1e-8f;
for (uint32_t iter = 0; iter < maxIter; iter++) {
// Deflate: zero out negligible subdiagonal elements
for (uint8_t i = p - 1; i > 0; i--) {
float test = fabsf(W[i][i - 1]);
float scale = fabsf(W[i - 1][i - 1]) + fabsf(W[i][i]);
if (test < tol * (scale + 1e-30f)) {
W[i][i - 1] = 0;
}
}
// Find the smallest unreduced block [start..end]
uint8_t start = 0, end = p - 1;
for (uint8_t i = 0; i < p - 1; i++) {
if (fabsf(W[i + 1][i]) >
tol * (fabsf(W[i][i]) + fabsf(W[i + 1][i + 1]) + 1e-30f)) {
start = i + 1;
}
}
for (int8_t i = (int8_t)p - 2; i >= 0; i--) {
if (fabsf(W[i + 1][i]) >
tol * (fabsf(W[i][i]) + fabsf(W[i + 1][i + 1]) + 1e-30f)) {
end = (uint8_t)i;
break;
}
}
// Check convergence of the block
if (start >= end) {
continue;
}
bool blockConverged = true;
for (uint8_t i = start; i <= end; i++) {
if (i > start && fabsf(W[i][i - 1]) > tol * (fabsf(W[i - 1][i - 1]) +
fabsf(W[i][i]) + 1e-30f)) {
blockConverged = false;
break;
}
if (i < end &&
fabsf(W[i][i + 1]) >
tol * (fabsf(W[i][i]) + fabsf(W[i + 1][i + 1]) + 1e-30f)) {
blockConverged = false;
break;
}
}
if (blockConverged)
continue;
// Wilkinson shift from bottom 2×2 corner
float a = W[end - 1][end - 1];
float b = W[end - 1][end];
float c = W[end][end - 1];
float d = W[end][end];
float trace = a + d;
float det = a * d - b * c;
float disc = trace * trace - 4.0f * det;
float shift;
if (disc >= 0) {
float sqrtDisc = sqrtf(disc);
float e1 = (trace + sqrtDisc) / 2.0f;
float e2 = (trace - sqrtDisc) / 2.0f;
shift = (fabsf(e1 - d) < fabsf(e2 - d)) ? e1 : e2;
} else {
shift = d;
}
// --- Implicit QR step using Givens rotations ---
// First, apply Givens rotation from the left to zero out (W[start][start-1]
// - shift) For the bidiagonal structure, we process from top to bottom.
float x = W[start][start] - shift;
float y = (start > 0) ? W[start][start - 1] : 0.0f;
for (uint8_t i = start; i <= end; i++) {
float r = sqrtf(x * x + y * y);
if (r < 1e-30f) {
x = W[i][i];
y = (i < end) ? W[i + 1][i] : 0.0f;
continue;
}
float cs = x / r;
float sn = y / r;
// Apply Givens from left to rows i, i+1 of W (columns i..p-1)
for (uint8_t j = i; j < p; j++) {
float t1 = W[i][j];
float t2 = W[i + 1][j];
W[i][j] = cs * t1 + sn * t2;
W[i + 1][j] = -sn * t1 + cs * t2;
}
// Apply Givens from right to columns i, i+1 of W (rows 0..i)
if (i > start) {
for (uint8_t j = 0; j <= i; j++) {
float t1 = W[j][i];
float t2 = W[j][i + 1];
W[j][i] = cs * t1 + sn * t2;
W[j][i + 1] = -sn * t1 + cs * t2;
}
}
// Accumulate right transformations into QR
for (uint8_t j = 0; j < n; j++) {
float t1 = QR[j][i];
float t2 = QR[j][i + 1];
QR[j][i] = cs * t1 + sn * t2;
QR[j][i + 1] = -sn * t1 + cs * t2;
}
// Prepare next Givens rotation
x = W[i + 1][i];
y = (i + 1 < end) ? W[i + 1][i + 1] : 0.0f;
}
}
// ---- Phase 3: Extract Results ----
// Singular values are the absolute values of diagonal elements of W
for (uint8_t i = 0; i < p; i++) {
sigma[i][0] = fabsf(W[i][i]);
}
// Sort singular values in descending order and reorder U, V accordingly
for (uint8_t i = 0; i < p - 1; i++) {
for (uint8_t j = i + 1; j < p; j++) {
if (sigma[j][0] > sigma[i][0]) {
float tmpS = sigma[i][0];
sigma[i][0] = sigma[j][0];
sigma[j][0] = tmpS;
// Swap columns of QL
for (uint8_t k = 0; k < 5; k++) {
float tmpQ = QL[k][i];
QL[k][i] = QL[k][j];
QL[k][j] = tmpQ;
}
// Swap columns of QR
for (uint8_t k = 0; k < 5; k++) {
float tmpQ = QR[k][i];
QR[k][i] = QR[k][j];
QR[k][j] = tmpQ;
}
}
}
}
// ---- Phase 4: Compute Final U and Vt ----
// If transposeNeeded (wide matrix), we computed SVD(Aᵀ) = Ũ·Σ·Ṽᵀ
// Then U = Ṽ (= QR[:,0:p]) and Vt = Ũᵀ (= QL[:,0:p]ᵀ)
// Otherwise, SVD(A) = QL[:,0:p] · Σ · (QR[:,0:p])ᵀ
// So U = QL[:,0:p] and Vt = QR[:,0:p]ᵀ
for (uint8_t i = 0; i < m; i++) {
for (uint8_t j = 0; j < n; j++) {
if (j < p) {
if (transposeNeeded) {
// U = QR[:, 0:p]ᵀ → U[i][j] = QR[j][i]
U[i][j] = QR[j][i];
} else {
// U = QL[:, 0:p]
U[i][j] = QL[i][j];
}
} else {
U[i][j] = 0;
}
}
}
for (uint8_t i = 0; i < n; i++) {
for (uint8_t j = 0; j < m; j++) {
if (i < p && j < m) {
if (transposeNeeded) {
// Vt = QL[:, 0:p]ᵀ → Vt[i][j] = QL[j][i]
Vt[i][j] = QL[j][i];
} else {
// Vt = QR[:, 0:p]ᵀ → Vt[i][j] = QR[j][i]
Vt[i][j] = QR[j][i];
}
} else {
Vt[i][j] = 0;
}
}
}
}
#endif
+131
View File
@@ -0,0 +1,131 @@
#pragma once
#include "Matrix.hpp"
/**
* @brief library that uses Matrix.hpp and performs SVD on a matrix
*/
namespace SVD {
/**
* @brief Compute the Singular Value Decomposition (SVD) of this matrix.
*
* Decomposes A into U × Σ × Vᵀ where:
* - U is an m×k orthogonal matrix (left singular vectors)
* - Σ is a k×k diagonal matrix with non-negative singular values
* (stored as a k×1 column vector)
* - Vᵀ is a k×n orthogonal matrix (right singular vectors, transposed)
* - k = min(m, n)
*
* The decomposition satisfies: A ≈ U × diag(Σ) × Vᵀ
* Singular values are returned in descending order.
*
* @param U Output: left singular vectors (m×k orthogonal matrix)
* @param sigma Output: singular values as k×1 column vector, sorted descending
* @param Vt Output: right singular vectors transposed (k×n matrix)
*
* @note This implementation uses the Golub-Kahan-Reinsch algorithm:
* 1. Householder bidiagonalization of A
* 2. Implicit QR iteration on the bidiagonal matrix
* 3. Accumulation of U and V factors throughout
*/
template <uint8_t rows, uint8_t columns>
void SVD(Matrix<rows, columns> &matrixToDecompose, Matrix<rows, columns> &U,
Matrix<columns, 1> &sigma, Matrix<columns, columns> &Vt);
// ========================================================================
// SVD Building Block Functions (for unit testing)
// These operate on internal 5×5 working arrays for maximum flexibility.
// ========================================================================
/**
* @brief Compute a Householder reflector vector.
*
* Given input vector x, computes normalized v and scalar alpha such that:
* (I - 2·v·vᵀ) · x = [alpha, 0, 0, ...]ᵀ
*
* @param x Input vector (up to 5 elements)
* @param len Number of valid elements in x
* @param v Output: normalized Householder vector (v[0] is the first element)
* @param alpha Output: the resulting first element after reflection
* @return The norm of the input vector x
*/
static float ComputeHouseholder(const float *x, uint8_t len, float *v,
float &alpha);
/**
* @brief Apply a Householder reflection from the left.
*
* Transforms W = (I - 2·v·vᵀ) · W where v operates on rows [startRow..endRow].
*
* @param W Input/output: matrix to transform (5×5 working array)
* @param v Householder vector (length = endRow - startRow + 1)
* @param startRow First row index
* @param endRow Last row index
*/
static void ApplyHouseholderLeft(Matrix<5, 5> &W, const float *v,
uint8_t startRow, uint8_t endRow);
/**
* @brief Apply a Householder reflection from the right.
*
* Transforms W = W · (I - 2·v·vᵀ) where v operates on columns
* [startCol..endCol].
*
* @param W Input/output: matrix to transform (5×5 working array)
* @param v Householder vector (length = endCol - startCol + 1)
* @param startCol First column index
* @param endCol Last column index
*/
static void ApplyHouseholderRight(Matrix<5, 5> &W, const float *v,
uint8_t startCol, uint8_t endCol);
/**
* @brief Compute a Givens rotation that zeros out y.
*
* Computes c, s such that:
* [c s] [x] = [r]
* [-s c] [y] [0]
* where r = sqrt(x² + y²).
*
* @param x First element
* @param y Second element (to be zeroed)
* @param c Output: cosine of rotation angle
* @param s Output: sine of rotation angle
*/
static void ComputeGivens(float x, float y, float &c, float &s);
/**
* @brief Apply a Givens rotation from the left to rows i and j.
*
* Applies [c s; -s c] to rows i, j of W (columns startCol..endCol).
*
* @param W Input/output: matrix to transform
* @param i First row index
* @param j Second row index
* @param c Cosine of rotation angle
* @param s Sine of rotation angle
* @param startCol First column to transform
* @param endCol Last column to transform
*/
static void ApplyGivensLeft(Matrix<5, 5> &W, uint8_t i, uint8_t j, float c,
float s, uint8_t startCol, uint8_t endCol);
/**
* @brief Apply a Givens rotation from the right to columns i and j.
*
* Applies [c -s; s c]ᵀ to columns i, j of W (rows startRow..endRow).
*
* @param W Input/output: matrix to transform
* @param i First column index
* @param j Second column index
* @param c Cosine of rotation angle
* @param s Sine of rotation angle
* @param startRow First row to transform
* @param endRow Last row to transform
*/
static void ApplyGivensRight(Matrix<5, 5> &W, uint8_t i, uint8_t j, float c,
float s, uint8_t startRow, uint8_t endRow);
} // namespace SVD
#ifndef SVD_H_
#include "SVD.cpp"
#endif // SVD_H_
+162
View File
@@ -0,0 +1,162 @@
#ifdef VECTOR3D_H_ // since the .cpp file has to be included by the .hpp file this
// will evaluate to true
#include <cmath>
#include <type_traits>
#include <string>
template <typename Type>
V3D<Type>::V3D(const Matrix<1, 3> &other)
{
this->x = other.Get(0, 0);
this->y = other.Get(0, 1);
this->z = other.Get(0, 2);
}
template <typename Type>
V3D<Type>::V3D(const Matrix<3, 1> &other)
{
this->x = other.Get(0, 0);
this->y = other.Get(1, 0);
this->z = other.Get(2, 0);
}
template <typename Type>
V3D<Type>::V3D(const V3D &other) : x(other.x),
y(other.y),
z(other.z)
{
static_assert(std::is_arithmetic<Type>::value, "Type must be a number");
}
template <typename Type>
V3D<Type>::V3D(Type x, Type y, Type z) : x(x),
y(y),
z(z)
{
static_assert(std::is_arithmetic<Type>::value, "Type must be a number");
}
template <typename Type>
template <typename OtherType>
V3D<Type>::V3D(const V3D<OtherType> &other)
{
static_assert(std::is_arithmetic<Type>::value, "Type must be a number");
static_assert(std::is_arithmetic<OtherType>::value, "OtherType must be a number");
this->x = static_cast<Type>(other.x);
this->y = static_cast<Type>(other.y);
this->z = static_cast<Type>(other.z);
}
template <typename Type>
std::array<Type, 3> V3D<Type>::ToArray() const
{
return {this->x, this->y, this->z};
}
template <typename Type>
void V3D<Type>::operator=(const V3D<Type> &other)
{
this->x = other.x;
this->y = other.y;
this->z = other.z;
}
template <typename Type>
V3D<Type> V3D<Type>::operator+(Type other) const
{
return V3D<Type>{this->x + other, this->y + other, this->z + other};
}
template <typename Type>
V3D<Type> V3D<Type>::operator+(const V3D<Type> &other) const
{
return V3D<Type>{this->x + other.x, this->y + other.y, this->z + other.z};
}
template <typename Type>
V3D<Type> V3D<Type>::operator-(Type other) const
{
return V3D<Type>{this->x - other, this->y - other, this->z - other};
}
template <typename Type>
V3D<Type> V3D<Type>::operator-(const V3D<Type> &other) const
{
return V3D<Type>{this->x - other.x, this->y - other.y, this->z - other.z};
}
template <typename Type>
V3D<Type> V3D<Type>::operator*(Type scalar) const
{
return V3D<Type>{this->x * scalar, this->y * scalar, this->z * scalar};
}
template <typename Type>
V3D<Type> V3D<Type>::operator/(Type scalar) const
{
return V3D<Type>{this->x / scalar, this->y / scalar, this->z / scalar};
}
template <typename Type>
V3D<Type> &V3D<Type>::operator+=(Type other)
{
*this = *this + other;
return *this;
}
template <typename Type>
V3D<Type> &V3D<Type>::operator+=(const V3D<Type> &other)
{
*this = *this + other;
return *this;
}
template <typename Type>
V3D<Type> &V3D<Type>::operator-=(Type other)
{
*this = *this - other;
return *this;
}
template <typename Type>
V3D<Type> &V3D<Type>::operator-=(const V3D<Type> &other)
{
*this = *this - other;
return *this;
}
template <typename Type>
V3D<Type> &V3D<Type>::operator/=(Type scalar)
{
if (scalar == 0)
{
return *this;
}
this->x /= scalar;
this->y /= scalar;
this->z /= scalar;
return *this;
}
template <typename Type>
V3D<Type> &V3D<Type>::operator*=(Type scalar)
{
this->x *= scalar;
this->y *= scalar;
this->z *= scalar;
return *this;
}
template <typename Type>
bool V3D<Type>::operator==(const V3D<Type> &other)
{
return this->x == other.x && this->y == other.y && this->z == other.z;
}
template <typename Type>
float V3D<Type>::magnitude()
{
return std::sqrt(static_cast<float>(this->x * this->x + this->y * this->y + this->z * this->z));
}
#endif // VECTOR3D_H_
+58
View File
@@ -0,0 +1,58 @@
#ifndef VECTOR3D_H_
#define VECTOR3D_H_
#include <cstdint>
#include "Matrix.hpp"
template <typename Type>
class V3D
{
public:
V3D(const Matrix<1, 3> &other);
V3D(const Matrix<3, 1> &other);
V3D(const V3D &other);
V3D(Type x = 0, Type y = 0, Type z = 0);
template <typename OtherType>
V3D(const V3D<OtherType> &other);
template <typename OtherType>
operator OtherType() const;
std::array<Type, 3> ToArray() const;
V3D<Type> operator+(Type other) const;
V3D<Type> operator+(const V3D<Type> &other) const;
V3D<Type> operator-(Type other) const;
V3D<Type> operator-(const V3D<Type> &other) const;
V3D<Type> operator*(Type scalar) const;
V3D<Type> operator/(Type scalar) const;
void operator=(const V3D<Type> &other);
V3D<Type> &operator+=(Type other);
V3D<Type> &operator+=(const V3D<Type> &other);
V3D<Type> &operator-=(Type other);
V3D<Type> &operator-=(const V3D<Type> &other);
V3D<Type> &operator/=(Type scalar);
V3D<Type> &operator*=(Type scalar);
bool operator==(const V3D<Type> &other);
float magnitude();
Type x;
Type y;
Type z;
};
#include "Vector3D.cpp"
#endif // VECTOR3D_H_
+46 -12
View File
@@ -1,21 +1,55 @@
cmake_minimum_required (VERSION 3.11)
# Quaternion tests
add_executable(quaternion-tests quaternion-tests.cpp)
project ("test_driver")
include(FetchContent)
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.0.1 # or a later release
target_link_libraries(quaternion-tests
PRIVATE
quaternion
Catch2::Catch2WithMain
)
FetchContent_MakeAvailable(Catch2)
# matrix tests
add_executable(matrix-tests matrix-tests.cpp)
target_link_libraries(matrix-tests
PRIVATE
Matrix
matrix
Catch2::Catch2WithMain
)
# matrix timing tests
add_executable(matrix-timing-tests matrix-timing-tests.cpp)
target_link_libraries(matrix-timing-tests
PRIVATE
matrix
Catch2::Catch2WithMain
)
# Vector 3D Tests
add_executable(vector-3d-tests vector-tests.cpp)
target_link_libraries(vector-3d-tests
PRIVATE
vector-3d
Catch2::Catch2WithMain
)
# SVD building block tests
add_executable(svd-build-blocks-tests svd-build-blocks-tests.cpp)
target_link_libraries(svd-build-blocks-tests
PRIVATE
matrix
svd
Catch2::Catch2WithMain
)
# SVD integration tests
add_executable(svd-integration-test svd-integration-test.cpp)
target_link_libraries(svd-integration-test
PRIVATE
matrix
svd
Catch2::Catch2WithMain
)
-14
View File
@@ -1,14 +0,0 @@
Addition: 0.419 s
Subtraction: 0.421 s
Multiplication: 3.297 s
Scalar Multiplication: 0.329 s
Element Multiply: 0.306 s
Element Divide: 0.302 s
Minor Matrix: 0.331 s
Determinant: 0.177 s
Matrix of Minors: 0.766 s
Invert: 0.183 s
Transpose: 0.215 s
Normalize: 0.315 s
GET ROW: 0.008 s
GET COLUMN: 0.43 s
File diff suppressed because it is too large Load Diff
+128
View File
@@ -0,0 +1,128 @@
// include the unit test framework first
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
// include the module you're going to test next
#include "Matrix.hpp"
// any other libraries
#include <array>
#include <cmath>
#include <cstdint>
// basically re-run all of the matrix tests with huge matrices and time the
// results.
TEST_CASE("Timing Tests", "Matrix") {
std::array<float, 50 * 50> arr1{};
for (uint16_t i{0}; i < 50 * 50; i++) {
arr1[i] = i;
}
std::array<float, 50 * 50> arr2{5, 6, 7, 8};
for (uint16_t i{50 * 50}; i < 2 * 50 * 50; i++) {
arr2[i] = i;
}
Matrix<50, 50> mat1{arr1};
Matrix<50, 50> mat2{arr2};
Matrix<50, 50> mat3{};
// A smaller matrix to use for really badly optimized operations
Matrix<4, 4> mat4{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
Matrix<4, 4> mat5{};
SECTION("Addition") {
for (uint32_t i{0}; i < 100000; i++) {
mat3 = mat1 + mat2;
}
}
SECTION("Subtraction") {
for (uint32_t i{0}; i < 100000; i++) {
mat3 = mat1 - mat2;
}
}
SECTION("Multiplication") {
for (uint32_t i{0}; i < 1000; i++) {
mat3 = mat1 * mat2;
}
}
SECTION("Scalar Multiplication") {
for (uint32_t i{0}; i < 100000; i++) {
mat3 = mat1 * 3;
}
}
SECTION("Element Multiply") {
for (uint32_t i{0}; i < 100000; i++) {
mat1.ElementMultiply(mat2, mat3);
}
}
SECTION("Element Divide") {
for (uint32_t i{0}; i < 100000; i++) {
mat1.ElementDivide(mat2, mat3);
}
}
SECTION("Minor Matrix") {
// what about matrices of 0,0 or 1,1?
// minor matrix for 2x2 matrix
Matrix<49, 49> minorMat1{};
for (uint32_t i{0}; i < 100000; i++) {
mat1.MinorMatrix(minorMat1, 0, 0);
}
}
SECTION("Determinant") {
for (uint32_t i{0}; i < 1000000; i++) {
float det = mat4.Det();
(void)det;
}
}
SECTION("Matrix of Minors") {
for (uint32_t i{0}; i < 1000000; i++) {
mat4.MatrixOfMinors(mat5);
}
}
SECTION("Invert") {
for (uint32_t i{0}; i < 1000000; i++) {
mat5 = mat4.Invert();
}
};
SECTION("Transpose") {
for (uint32_t i{0}; i < 100000; i++) {
mat3 = mat1.Transpose();
}
}
SECTION("Normalize") {
for (uint32_t i{0}; i < 100000; i++) {
mat3 = mat1 / mat1.EuclideanNorm();
}
}
SECTION("GET ROW") {
Matrix<1, 50> mat1Rows{};
for (uint32_t i{0}; i < 100000000; i++) {
mat1.GetRow(0, mat1Rows);
}
}
SECTION("GET COLUMN") {
Matrix<50, 1> mat1Columns{};
for (uint32_t i{0}; i < 100000000; i++) {
mat1.GetColumn(0, mat1Columns);
}
}
SECTION("QR Decomposition") {
Matrix<50, 50> Q, R{};
for (uint32_t i{0}; i < 500; i++) {
mat1.QRDecomposition(Q, R);
}
}
}
+103
View File
@@ -0,0 +1,103 @@
// include the unit test framework first
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
// include the module you're going to test next
#include "Quaternion.h"
// any other libraries
#include <array>
#include <cmath>
#include <iostream>
TEST_CASE("Vector Math", "Vector")
{
Quaternion q1{1, 2, 3, 4};
Quaternion q2{5, 6, 7, 8};
SECTION("Initialization")
{
// explicit initialization
REQUIRE(q1.w == 1);
REQUIRE(q1.v1 == 2);
REQUIRE(q1.v2 == 3);
REQUIRE(q1.v3 == 4);
// fill initialization
Quaternion q3{0};
REQUIRE(q3.w == 0);
REQUIRE(q3.v1 == 0);
REQUIRE(q3.v2 == 0);
REQUIRE(q3.v3 == 0);
// copy initialization
Quaternion q4{q1};
REQUIRE(q4.w == 1);
REQUIRE(q4.v1 == 2);
REQUIRE(q4.v2 == 3);
REQUIRE(q4.v3 == 4);
// matrix initialization
Matrix<1, 4> m1{1, 2, 3, 4};
Quaternion q5{m1};
REQUIRE(q5.w == 1);
REQUIRE(q5.v1 == 2);
REQUIRE(q5.v2 == 3);
REQUIRE(q5.v3 == 4);
// array initialization
Quaternion q6{std::array<float, 4>{1, 2, 3, 4}};
REQUIRE(q6.w == 1);
REQUIRE(q6.v1 == 2);
REQUIRE(q6.v2 == 3);
REQUIRE(q6.v3 == 4);
}
SECTION("Equals")
{
Quaternion q3{0, 0, 0, 0};
q3 = q1;
REQUIRE(q3.w == 1);
REQUIRE(q3.v1 == 2);
REQUIRE(q3.v2 == 3);
REQUIRE(q3.v3 == 4);
}
SECTION("Array access")
{
REQUIRE(q1[0] == 1);
REQUIRE(q1[1] == 2);
REQUIRE(q1[2] == 3);
REQUIRE(q1[3] == 4);
}
SECTION("Addition")
{
Quaternion q3 = q1 + q2;
REQUIRE(q3.w == 6);
REQUIRE(q3.v1 == 8);
REQUIRE(q3.v2 == 10);
REQUIRE(q3.v3 == 12);
}
SECTION("Multiplication")
{
Quaternion q3;
q1.Q_Mult(q2, q3);
REQUIRE(q3.w == -60);
REQUIRE(q3.v1 == 12);
REQUIRE(q3.v2 == 30);
REQUIRE(q3.v3 == 24);
}
SECTION("Rotation")
{
Quaternion q3{Quaternion::FromAngleAndAxis(M_PI / 2, Matrix<1, 3>{0, 0, 1})};
Quaternion q4{0, 1, 0, 0};
Quaternion q5;
q3.Rotate(q4, q5);
REQUIRE_THAT(q5.v1, Catch::Matchers::WithinRel(0.0f, 1e-6f));
REQUIRE_THAT(q5.v2, Catch::Matchers::WithinRel(1.0f, 1e-6f));
REQUIRE_THAT(q5.v3, Catch::Matchers::WithinRel(0.0f, 1e-6f));
}
}
-7
View File
@@ -1,7 +0,0 @@
# be in the root folder of this project when you run this
cd build/
ninja matrix-tests
echo "Running tests. This will take a while."
./unit-tests/matrix-tests -n "Timing Tests" -d yes > ../unit-tests/matrix-test-timings-temp.txt
cd ../unit-tests/
python3 test-timing-post-process.py
+785
View File
@@ -0,0 +1,785 @@
// include the unit test framework first
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
// include the module you're going to test next
#include "Matrix.hpp"
#include "SVD.hpp"
// any other libraries
#include <array>
#include <cmath>
#include <iostream>
// ============================================================================
// Helper: Frobenius norm of a 5×5 matrix
// ============================================================================
static float frobeniusNorm5(const Matrix<5, 5> &M) {
float sum = 0.0f;
for (uint8_t i = 0; i < 5; i++) {
for (uint8_t j = 0; j < 5; j++) {
float v = M.Get(i, j);
sum += v * v;
}
}
return sqrtf(sum);
}
// ============================================================================
// Helper: Check if a matrix is orthogonal (Mᵀ·M ≈ I)
// ============================================================================
static bool isOrthogonal5(const Matrix<5, 5> &M, float tol = 1e-6f) {
Matrix<5, 5> Mt = M.Transpose();
Matrix<5, 5> MtM{0};
Mt.Mult(M, MtM);
for (uint8_t i = 0; i < 5; i++) {
for (uint8_t j = 0; j < 5; j++) {
float expected = (i == j) ? 1.0f : 0.0f;
if (fabsf(MtM.Get(i, j) - expected) > tol) {
return false;
}
}
}
return true;
}
// ============================================================================
// TEST 1: ComputeHouseholder
// ============================================================================
TEST_CASE("SVD Building Block: ComputeHouseholder", "[Matrix][SVD]") {
// Test case: [3, 4] should give alpha = -5 (norm), v normalized
// Reference: scipy.linalg.householder([3, 4]) → v ≈ [0.894427191,
// 0.447213596], α = -5
{
float x[] = {3.0f, 4.0f};
float v[5] = {0};
float alpha = 0;
float norm = SVD::ComputeHouseholder(x, 2, v, alpha);
// Norm should be 5.0
REQUIRE_THAT(norm, Catch::Matchers::WithinRel(5.0f, 1e-6f));
// Alpha should be -5 (negative norm)
REQUIRE_THAT(alpha, Catch::Matchers::WithinRel(-5.0f, 1e-6f));
// v should be normalized: ||v|| ≈ 1
float vNorm = sqrtf(v[0] * v[0] + v[1] * v[1]);
REQUIRE_THAT(vNorm, Catch::Matchers::WithinRel(1.0f, 1e-6f));
// Verify H·x = [alpha, 0]: (I - 2vvᵀ)·x should give [-5, 0]
float hx0 = x[0] - 2.0f * v[0] * (v[0] * x[0] + v[1] * x[1]);
float hx1 = x[1] - 2.0f * v[1] * (v[0] * x[0] + v[1] * x[1]);
REQUIRE_THAT(hx0, Catch::Matchers::WithinRel(alpha, 1e-6f));
REQUIRE_THAT(hx1, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
}
// Test case: [1, 3]
// Reference: norm = √10 ≈ 3.16228, alpha = -√10
{
float x[] = {1.0f, 3.0f};
float v[5] = {0};
float alpha = 0;
float norm = SVD::ComputeHouseholder(x, 2, v, alpha);
REQUIRE_THAT(norm, Catch::Matchers::WithinRel(sqrtf(10.0f), 1e-6f));
REQUIRE_THAT(alpha, Catch::Matchers::WithinRel(-sqrtf(10.0f), 1e-6f));
// Verify H·x = [alpha, 0]
float dot = v[0] * x[0] + v[1] * x[1];
float hx0 = x[0] - 2.0f * v[0] * dot;
float hx1 = x[1] - 2.0f * v[1] * dot;
REQUIRE_THAT(hx0, Catch::Matchers::WithinRel(alpha, 1e-6f));
REQUIRE_THAT(hx1, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
}
// Test case: [1, 2, 3] (3D)
// Reference: norm = √14 ≈ 3.74166
{
float x[] = {1.0f, 2.0f, 3.0f};
float v[5] = {0};
float alpha = 0;
float norm = SVD::ComputeHouseholder(x, 3, v, alpha);
REQUIRE_THAT(norm, Catch::Matchers::WithinRel(sqrtf(14.0f), 1e-6f));
REQUIRE_THAT(alpha, Catch::Matchers::WithinRel(-sqrtf(14.0f), 1e-6f));
// Verify v is normalized
float vNorm = sqrtf(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
REQUIRE_THAT(vNorm, Catch::Matchers::WithinRel(1.0f, 1e-6f));
// Verify H·x = [alpha, 0, 0]
float dot = v[0] * x[0] + v[1] * x[1] + v[2] * x[2];
for (uint8_t i = 0; i < 3; i++) {
float hx_i = x[i] - 2.0f * v[i] * dot;
if (i == 0) {
REQUIRE_THAT(hx_i, Catch::Matchers::WithinRel(alpha, 1e-6f));
} else {
REQUIRE_THAT(hx_i, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
}
}
}
// Test case: [0, 0, 1] (already has leading zeros)
{
float x[] = {0.0f, 0.0f, 1.0f};
float v[5] = {0};
float alpha = 0;
float norm = SVD::ComputeHouseholder(x, 3, v, alpha);
REQUIRE_THAT(norm, Catch::Matchers::WithinRel(1.0f, 1e-6f));
REQUIRE_THAT(alpha, Catch::Matchers::WithinRel(-1.0f, 1e-6f));
// Verify H·x = [-1, 0, 0]
float dot = v[0] * x[0] + v[1] * x[1] + v[2] * x[2];
float hx0 = x[0] - 2.0f * v[0] * dot;
float hx1 = x[1] - 2.0f * v[1] * dot;
float hx2 = x[2] - 2.0f * v[2] * dot;
REQUIRE_THAT(hx0, Catch::Matchers::WithinRel(alpha, 1e-6f));
REQUIRE_THAT(hx1, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
REQUIRE_THAT(hx2, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
}
// Test case: [5, -3, 2, 1] (4D)
{
float x[] = {5.0f, -3.0f, 2.0f, 1.0f};
float v[5] = {0};
float alpha = 0;
float norm = SVD::ComputeHouseholder(x, 4, v, alpha);
REQUIRE_THAT(norm, Catch::Matchers::WithinRel(sqrtf(39.0f), 1e-6f));
REQUIRE_THAT(alpha, Catch::Matchers::WithinRel(-sqrtf(39.0f), 1e-6f));
// Verify H·x = [alpha, 0, 0, 0]
float dot = v[0] * x[0] + v[1] * x[1] + v[2] * x[2] + v[3] * x[3];
for (uint8_t i = 0; i < 4; i++) {
float hx_i = x[i] - 2.0f * v[i] * dot;
if (i == 0) {
REQUIRE_THAT(hx_i, Catch::Matchers::WithinRel(alpha, 1e-6f));
} else {
REQUIRE_THAT(hx_i, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
}
}
}
// Test case: zero vector
{
float x[] = {0.0f, 0.0f};
float v[5] = {0};
float alpha = 0;
float norm = SVD::ComputeHouseholder(x, 2, v, alpha);
REQUIRE_THAT(norm, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
REQUIRE(alpha == 0.0f);
}
}
// ============================================================================
// TEST 2: ApplyHouseholderLeft
// ============================================================================
TEST_CASE("SVD Building Block: ApplyHouseholderLeft", "[Matrix][SVD]") {
// Test: Apply Householder to zero out column 0, rows 1:2 of a 3×3 matrix
// Input: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
// After applying HH on col 0 (rows 1:2): A[2,0] should be ~0
{
Matrix<5, 5> W{1.0f, 2.0f, 3.0f, 0, 0, 4.0f, 5.0f, 6.0f, 0,
0, 7.0f, 8.0f, 9.0f, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0};
// Compute Householder for column 0, rows 1:2 → vector [4, 7]
float x[] = {4.0f, 7.0f};
float v[5] = {0};
float alpha = 0;
SVD::ComputeHouseholder(x, 2, v, alpha);
// Apply from left
SVD::ApplyHouseholderLeft(W, v, 1, 2);
// A[2,0] should be ~0
REQUIRE_THAT(W.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
// Verify orthogonality of the transformation: W = H·W_original
Matrix<5, 5> W_orig{1.0f, 2.0f, 3.0f, 0, 0, 4.0f, 5.0f, 6.0f, 0,
0, 7.0f, 8.0f, 9.0f, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0};
// Compute H_left explicitly: I - 2*v*vᵀ (on rows 1:2)
Matrix<5, 5> H_left{0};
for (uint8_t i = 0; i < 5; i++) {
H_left[i][i] = 1.0f;
}
// Apply -2*v*vᵀ to the sub-block
float vv = v[0] * v[0] + v[1] * v[1];
for (uint8_t i = 1; i <= 2; i++) {
for (uint8_t j = 1; j <= 2; j++) {
H_left[i][j] -= 2.0f * v[i - 1] * v[j - 1] / vv;
}
}
// Verify: W ≈ H_left · W_orig
Matrix<5, 5> HLeftW{0};
H_left.Mult(W_orig, HLeftW);
float err = frobeniusNorm5(W - HLeftW);
REQUIRE_THAT(err, Catch::Matchers::WithinAbs(0.0f, 1e-4f));
// Verify H_left is orthogonal
REQUIRE(isOrthogonal5(H_left));
}
// Test: Apply to a larger block (4 rows)
{
Matrix<5, 5> W{1.0f, 2.0f, 0, 0, 0, 3.0f, 4.0f, 0, 0,
0, 5.0f, 6.0f, 0, 0, 0, 7.0f, 8.0f, 0,
0, 0, 0, 0, 0, 0, 0};
// Householder on [3, 5, 7] (rows 1:3)
float x[] = {3.0f, 5.0f, 7.0f};
float v[5] = {0};
float alpha = 0;
SVD::ComputeHouseholder(x, 3, v, alpha);
SVD::ApplyHouseholderLeft(W, v, 1, 3);
// A[2,0] and A[3,0] should be ~0
REQUIRE_THAT(W.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
REQUIRE_THAT(W.Get(3, 0), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
}
}
// ============================================================================
// TEST 3: ApplyHouseholderRight
// ============================================================================
TEST_CASE("SVD Building Block: ApplyHouseholderRight", "[Matrix][SVD]") {
// Test: Apply Householder to zero out row 0, cols 1:2 of a 3×3 matrix
// Input: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
// After applying HH on row 0 (cols 1:2): A[0,2] should be ~0
{
Matrix<5, 5> W{1.0f, 2.0f, 3.0f, 0, 0, 4.0f, 5.0f, 6.0f, 0,
0, 7.0f, 8.0f, 9.0f, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0};
// Householder for row 0, cols 1:2 → vector [2, 3]
float x[] = {2.0f, 3.0f};
float v[5] = {0};
float alpha = 0;
SVD::ComputeHouseholder(x, 2, v, alpha);
// Apply from right
SVD::ApplyHouseholderRight(W, v, 1, 2);
// A[0,2] should be ~0
REQUIRE_THAT(W.Get(0, 2), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
// Verify W ≈ W_orig · H_right
Matrix<5, 5> W_orig{1.0f, 2.0f, 3.0f, 0, 0, 4.0f, 5.0f, 6.0f, 0,
0, 7.0f, 8.0f, 9.0f, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0};
// Compute H_right = I - 2*v*vᵀ (on cols 1:2)
Matrix<5, 5> H_right{0};
for (uint8_t i = 0; i < 5; i++) {
H_right[i][i] = 1.0f;
}
float vv = v[0] * v[0] + v[1] * v[1];
for (uint8_t i = 1; i <= 2; i++) {
for (uint8_t j = 1; j <= 2; j++) {
H_right[i][j] -= 2.0f * v[i - 1] * v[j - 1] / vv;
}
}
Matrix<5, 5> WOrigH{0};
W_orig.Mult(H_right, WOrigH);
float err = frobeniusNorm5(W - WOrigH);
REQUIRE_THAT(err, Catch::Matchers::WithinAbs(0.0f, 1e-4f));
// Verify H_right is orthogonal
REQUIRE(isOrthogonal5(H_right));
}
// Test: Apply to wider block (4 cols)
{
Matrix<5, 5> W{1.0f, 2.0f, 3.0f, 4.0f, 0, 5.0f, 6.0f, 7.0f, 8.0f,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0};
// Householder on [2, 3, 4] (cols 1:3)
float x[] = {2.0f, 3.0f, 4.0f};
float v[5] = {0};
float alpha = 0;
SVD::ComputeHouseholder(x, 3, v, alpha);
SVD::ApplyHouseholderRight(W, v, 1, 3);
// A[0,2] and A[0,3] should be ~0
REQUIRE_THAT(W.Get(0, 2), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
REQUIRE_THAT(W.Get(0, 3), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
}
}
// ============================================================================
// TEST 4: ComputeGivens
// ============================================================================
TEST_CASE("SVD Building Block: ComputeGivens", "[Matrix][SVD]") {
// Test case: [3, 4] → c = 0.6, s = 0.8 (3-4-5 triangle)
{
float c, s;
SVD::ComputeGivens(3.0f, 4.0f, c, s);
REQUIRE_THAT(c, Catch::Matchers::WithinRel(0.6f, 1e-6f));
REQUIRE_THAT(s, Catch::Matchers::WithinRel(0.8f, 1e-6f));
// Verify: [c s; -s c] · [3; 4] = [5; 0]
float r = c * 3.0f + s * 4.0f;
float z = -s * 3.0f + c * 4.0f;
REQUIRE_THAT(r, Catch::Matchers::WithinRel(5.0f, 1e-6f));
REQUIRE_THAT(z, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
// Verify c² + s² = 1
REQUIRE_THAT(c * c + s * s, Catch::Matchers::WithinRel(1.0f, 1e-6f));
}
// Test case: [1, 0] → c = 1, s = 0
{
float c, s;
SVD::ComputeGivens(1.0f, 0.0f, c, s);
REQUIRE_THAT(c, Catch::Matchers::WithinRel(1.0f, 1e-6f));
REQUIRE_THAT(s, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
}
// Test case: [0, 5] → c = 0, s = 1
{
float c, s;
SVD::ComputeGivens(0.0f, 5.0f, c, s);
REQUIRE_THAT(c, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
REQUIRE_THAT(s, Catch::Matchers::WithinRel(1.0f, 1e-6f));
// Verify: [c s; -s c] · [0; 5] = [5; 0]
float r = c * 0.0f + s * 5.0f;
float z = -s * 0.0f + c * 5.0f;
REQUIRE_THAT(r, Catch::Matchers::WithinRel(5.0f, 1e-6f));
REQUIRE_THAT(z, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
}
// Test case: [-3, -4] → c = -0.6, s = -0.8
{
float c, s;
SVD::ComputeGivens(-3.0f, -4.0f, c, s);
REQUIRE_THAT(c, Catch::Matchers::WithinRel(-0.6f, 1e-6f));
REQUIRE_THAT(s, Catch::Matchers::WithinRel(-0.8f, 1e-6f));
// Verify: [c s; -s c] · [-3; -4] = [5; 0]
float r = c * (-3.0f) + s * (-4.0f);
float z = -s * (-3.0f) + c * (-4.0f);
REQUIRE_THAT(r, Catch::Matchers::WithinRel(5.0f, 1e-6f));
REQUIRE_THAT(z, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
}
// Test case: [1, -1] → c = 1/√2, s = -1/√2 (45°)
{
float c, s;
SVD::ComputeGivens(1.0f, -1.0f, c, s);
float invSqrt2 = 1.0f / sqrtf(2.0f);
REQUIRE_THAT(c, Catch::Matchers::WithinRel(invSqrt2, 1e-6f));
REQUIRE_THAT(s, Catch::Matchers::WithinRel(-invSqrt2, 1e-6f));
// Verify: [c s; -s c] · [1; -1] = [√2; 0]
float r = c * 1.0f + s * (-1.0f);
float z = -s * 1.0f + c * (-1.0f);
REQUIRE_THAT(r, Catch::Matchers::WithinRel(sqrtf(2.0f), 1e-6f));
REQUIRE_THAT(z, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
}
// Test case: [0, 0] → c = 1, s = 0 (identity)
{
float c, s;
SVD::ComputeGivens(0.0f, 0.0f, c, s);
REQUIRE_THAT(c, Catch::Matchers::WithinRel(1.0f, 1e-6f));
REQUIRE_THAT(s, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
}
// Test case: [7, 24] → c = 7/25, s = 24/25 (7-24-25 triangle)
{
float c, s;
SVD::ComputeGivens(7.0f, 24.0f, c, s);
REQUIRE_THAT(c, Catch::Matchers::WithinRel(7.0f / 25.0f, 1e-6f));
REQUIRE_THAT(s, Catch::Matchers::WithinRel(24.0f / 25.0f, 1e-6f));
float r = c * 7.0f + s * 24.0f;
float z = -s * 7.0f + c * 24.0f;
REQUIRE_THAT(r, Catch::Matchers::WithinRel(25.0f, 1e-6f));
REQUIRE_THAT(z, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
}
}
// ============================================================================
// TEST 5: ApplyGivensLeft
// ============================================================================
TEST_CASE("SVD Building Block: ApplyGivensLeft", "[Matrix][SVD]") {
// Test: Apply Givens to zero out W[1,0] of a 2×2 matrix
// Input: [[3, 4], [1, 2]]
// Givens on rows 0,1 with x=W[0,0]=3, y=W[1,0]=1
{
Matrix<5, 5> W{3.0f, 4.0f, 0, 0, 0, 1.0f, 2.0f, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
float c, s;
SVD::ComputeGivens(3.0f, 1.0f, c, s);
SVD::ApplyGivensLeft(W, 0, 1, c, s, 0, 4);
// W[1,0] should be ~0
REQUIRE_THAT(W.Get(1, 0), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
// Verify W ≈ G · W_orig
Matrix<5, 5> W_orig{3.0f, 4.0f, 0, 0, 0, 1.0f, 2.0f, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
// Givens rotation matrix (5×5)
Matrix<5, 5> G{0};
for (uint8_t i = 0; i < 5; i++) {
G[i][i] = 1.0f;
}
G[0][0] = c;
G[0][1] = s;
G[1][0] = -s;
G[1][1] = c;
Matrix<5, 5> GW{0};
G.Mult(W_orig, GW);
float err = frobeniusNorm5(W - GW);
REQUIRE_THAT(err, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
// Verify G is orthogonal
REQUIRE(isOrthogonal5(G));
}
// Test: Apply to larger range of columns
{
Matrix<5, 5> W{3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 1.0f, 2.0f, 3.0f, 4.0f,
5.0f, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0};
float c, s;
SVD::ComputeGivens(3.0f, 1.0f, c, s);
SVD::ApplyGivensLeft(W, 0, 1, c, s, 0, 4);
REQUIRE_THAT(W.Get(1, 0), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
}
}
// ============================================================================
// TEST 6: ApplyGivensRight
// ============================================================================
TEST_CASE("SVD Building Block: ApplyGivensRight", "[Matrix][SVD]") {
// Test: Apply Givens to zero out W[0,1] of a 2×2 matrix
// Input: [[3, 4], [1, 2]]
// Givens on cols 0,1 with x=W[0,0]=3, y=W[0,1]=4
{
Matrix<5, 5> W{3.0f, 4.0f, 0, 0, 0, 1.0f, 2.0f, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
float c, s;
SVD::ComputeGivens(3.0f, 4.0f, c, s);
SVD::ApplyGivensRight(W, 0, 1, c, s, 0, 4);
// W[0,1] should be ~0
REQUIRE_THAT(W.Get(0, 1), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
// Verify W ≈ W_orig · G
Matrix<5, 5> W_orig{3.0f, 4.0f, 0, 0, 0, 1.0f, 2.0f, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
// Givens rotation matrix (5×5)
Matrix<5, 5> G{0};
for (uint8_t i = 0; i < 5; i++) {
G[i][i] = 1.0f;
}
G[0][0] = c;
G[0][1] = -s;
G[1][0] = s;
G[1][1] = c;
Matrix<5, 5> WG{0};
W_orig.Mult(G, WG);
float err = frobeniusNorm5(W - WG);
REQUIRE_THAT(err, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
// Verify G is orthogonal
REQUIRE(isOrthogonal5(G));
}
// Test: Apply to larger range of rows
{
Matrix<5, 5> W{3.0f, 4.0f, 0, 0, 0, 1.0f, 2.0f, 0, 0, 0, 5.0f, 6.0f, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
float c, s;
SVD::ComputeGivens(3.0f, 4.0f, c, s);
SVD::ApplyGivensRight(W, 0, 1, c, s, 0, 2);
REQUIRE_THAT(W.Get(0, 1), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
}
}
// ============================================================================
// TEST 7: Full Bidiagonalization (composing Householder steps)
// ============================================================================
TEST_CASE("SVD Building Block: Householder Bidiagonalization",
"[Matrix][SVD]") {
// Test: Bidiagonalize a 3×3 matrix and verify reconstruction
// Input: [[1, 2, 3], [4, 5, 6], [7, 8, 10]]
{
Matrix<5, 5> W{1.0f, 2.0f, 3.0f, 0, 0, 4.0f, 5.0f, 6.0f, 0,
0, 7.0f, 8.0f, 10.0f, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0};
// Step 1: Left HH on column 0, rows 1:2 → zero out W[2,0]
{
float x[] = {4.0f, 7.0f};
float v[5] = {0};
float alpha = 0;
SVD::ComputeHouseholder(x, 2, v, alpha);
SVD::ApplyHouseholderLeft(W, v, 1, 2);
}
// Step 2: Right HH on row 0, cols 1:2 → zero out W[0,2]
{
float x[] = {W.Get(0, 1), W.Get(0, 2)};
float v[5] = {0};
float alpha = 0;
SVD::ComputeHouseholder(x, 2, v, alpha);
SVD::ApplyHouseholderRight(W, v, 1, 2);
}
// Step 3: Left HH on column 1, rows 2:2 → nothing to do (single element)
// Verify bidiagonal structure: for 3x3, zero elements are A[2][0] (below
// subdiag in col 0) and A[0][2] (above superdiag in row 0) A[2][1] is the
// subdiagonal element of col 1 — valid in bidiagonal form
REQUIRE_THAT(W.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-4f));
REQUIRE_THAT(W.Get(0, 2), Catch::Matchers::WithinAbs(0.0f, 1e-4f));
}
// Test: Bidiagonalize a 4×3 matrix
{
Matrix<5, 5> W{1.0f, 2.0f, 3.0f, 0, 0, 4.0f, 5.0f, 6.0f, 0,
0, 7.0f, 8.0f, 9.0f, 0, 0, 10.0f, 11.0f, 12.0f,
0, 0, 0, 0, 0, 0, 0};
// Step 1: Left HH on col 0, rows 1:3 → zero out W[2,0], W[3,0]
{
float x[] = {4.0f, 7.0f, 10.0f};
float v[5] = {0};
float alpha = 0;
SVD::ComputeHouseholder(x, 3, v, alpha);
SVD::ApplyHouseholderLeft(W, v, 1, 3);
}
// Step 2: Right HH on row 0, cols 1:2 → zero out W[0,2]
{
float x[] = {W.Get(0, 1), W.Get(0, 2)};
float v[5] = {0};
float alpha = 0;
SVD::ComputeHouseholder(x, 2, v, alpha);
SVD::ApplyHouseholderRight(W, v, 1, 2);
}
// Step 3: Left HH on col 1, rows 2:3 → zero out W[3,1]
{
float x[] = {W.Get(2, 1), W.Get(3, 1)};
float v[5] = {0};
float alpha = 0;
SVD::ComputeHouseholder(x, 2, v, alpha);
SVD::ApplyHouseholderLeft(W, v, 2, 3);
}
// Verify bidiagonal structure
REQUIRE_THAT(W.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
REQUIRE_THAT(W.Get(3, 0), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
REQUIRE_THAT(W.Get(3, 1), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
}
// Test: Diagonal matrix (no transformations needed)
{
Matrix<5, 5> W{10.0f, 0, 0, 0, 0, 0, 5.0f, 0, 0, 0, 0, 0, 2.0f,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
// Householder on zero vector should be identity
float x[] = {0.0f, 0.0f};
float v[5] = {0};
float alpha = 0;
SVD::ComputeHouseholder(x, 2, v, alpha);
// Applying identity should not change anything
Matrix<5, 5> W_copy{10.0f, 0, 0, 0, 0, 0, 5.0f, 0, 0, 0, 0, 0, 2.0f,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
SVD::ApplyHouseholderLeft(W_copy, v, 1, 2);
REQUIRE_THAT(frobeniusNorm5(W - W_copy),
Catch::Matchers::WithinAbs(0.0f, 1e-6f));
}
}
// ============================================================================
// //
// ============================================================================
// TEST 8: Givens QR step on bidiagonal matrix
// ===========================================================================
TEST_CASE("SVD Building Block: Givens QR Step on Bidiagonal", "[Matrix][SVD]") {
// Test: Apply left Givens to zero subdiagonal of a bidiagonal matrix,
// then apply right Givens with restricted row range to restore bidiagonal
// form.
//
// Input: 3x3 bidiagonal [[1, 2, 0], [3, -4, 5], [0, 6, -7]]
// Step 1: Left Givens on rows 0,1 with x=W[0][0]=1, y=W[1][0]=3 -> zero
// W[1][0] Step 2: Right Givens on cols 1,2 with x=W[0][1], y=W[0][2] -> zero
// W[0][2]
// Only applied to row 0 (to not reintroduce subdiagonal non-zeros)
{
Matrix<5, 5> W{1.0f, 2.0f, 0.0f, 0, 0, 3.0f, -4.0f, 5.0f, 0,
0, 0.0f, 6.0f, -7.0f, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0};
float c, s;
SVD::ComputeGivens(W.Get(0, 0), W.Get(1, 0), c, s);
// Apply from left to zero subdiagonal at W[1][0]
SVD::ApplyGivensLeft(W, 0, 1, c, s, 0, 4);
REQUIRE_THAT(W.Get(1, 0), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
// After left Givens, W[0][2] may have become non-zero (fill-in from row 0)
// Apply right Givens to cols 1,2 with x=W[0][1], y=W[0][2] -> zero W[0][2]
// Only apply to rows 0 (to preserve bidiagonal structure below row 0)
float c2, s2;
SVD::ComputeGivens(W.Get(0, 1), W.Get(0, 2), c2, s2);
SVD::ApplyGivensRight(W, 1, 2, c2, s2, 0, 0);
// Should be bidiagonal: W[1][0] ~ 0 (from left Givens), W[0][2] ~ 0 (from
// right Givens)
REQUIRE_THAT(W.Get(1, 0), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
REQUIRE_THAT(W.Get(0, 2), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
}
// Test: Verify that a full QR step (left + right Givens) preserves the
// bidiagonal structure when applied correctly with proper row ranges.
{
Matrix<5, 5> W{2.0f, 3.0f, 0, 0, 0, -1.0f, 4.0f, 5.0f, 0, 0, 0, 6.0f, -7.0f,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
// Left Givens on col 0 (rows 0,1)
float c, s;
SVD::ComputeGivens(W.Get(0, 0), W.Get(1, 0), c, s);
SVD::ApplyGivensLeft(W, 0, 1, c, s, 0, 4);
REQUIRE_THAT(W.Get(1, 0), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
// Right Givens on row 0 (cols 1,2) - only affect row 0
float c2, s2;
SVD::ComputeGivens(W.Get(0, 1), W.Get(0, 2), c2, s2);
SVD::ApplyGivensRight(W, 1, 2, c2, s2, 0, 0);
// Bidiagonal structure preserved
REQUIRE_THAT(W.Get(1, 0), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
REQUIRE_THAT(W.Get(0, 2), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
}
}
// ============================================================================TEST
// 9: Orthogonality preservation of Householder transformations
// ============================================================================
TEST_CASE("SVD Building Block: Householder preserves orthogonality",
"[Matrix][SVD]") {
// Starting with an orthogonal matrix, applying Householder should preserve it
{
// Identity matrix is orthogonal
Matrix<5, 5> M{0};
for (uint8_t i = 0; i < 5; i++) {
M[i][i] = 1.0f;
}
// Householder on first 3 elements of column 0
float x[] = {1.0f, 0.0f, 0.0f};
float v[5] = {0};
float alpha = 0;
SVD::ComputeHouseholder(x, 3, v, alpha);
// Apply from left
Matrix<5, 5> M_left = M;
SVD::ApplyHouseholderLeft(M_left, v, 0, 2);
// M_left should still be orthogonal
REQUIRE(isOrthogonal5(M_left));
// Apply from right
Matrix<5, 5> M_right = M;
SVD::ApplyHouseholderRight(M_right, v, 0, 2);
REQUIRE(isOrthogonal5(M_right));
}
// Random orthogonal matrix (rotation)
{
float c = sqrtf(0.5f);
float s = sqrtf(0.5f);
Matrix<5, 5> M{0};
M[0][0] = c;
M[0][1] = -s;
M[1][0] = s;
M[1][1] = c;
for (uint8_t i = 2; i < 5; i++) {
M[i][i] = 1.0f;
}
REQUIRE(isOrthogonal5(M));
// Apply Householder on rows 0,1
float x[] = {c, s};
float v[5] = {0};
float alpha = 0;
SVD::ComputeHouseholder(x, 2, v, alpha);
Matrix<5, 5> M_test = M;
SVD::ApplyHouseholderLeft(M_test, v, 0, 1);
REQUIRE(isOrthogonal5(M_test));
}
}
// ============================================================================
// TEST 10: Orthogonality preservation of Givens transformations
// ============================================================================
TEST_CASE("SVD Building Block: Givens preserves orthogonality",
"[Matrix][SVD]") {
// Starting with an orthogonal matrix, applying Givens should preserve it
{
Matrix<5, 5> M{0};
for (uint8_t i = 0; i < 5; i++) {
M[i][i] = 1.0f;
}
float c, s;
SVD::ComputeGivens(3.0f, 4.0f, c, s);
// Apply from left
Matrix<5, 5> M_left = M;
SVD::ApplyGivensLeft(M_left, 0, 1, c, s, 0, 4);
REQUIRE(isOrthogonal5(M_left));
// Apply from right
Matrix<5, 5> M_right = M;
SVD::ApplyGivensRight(M_right, 0, 1, c, s, 0, 4);
REQUIRE(isOrthogonal5(M_right));
}
}
+269
View File
@@ -0,0 +1,269 @@
#include "Matrix.hpp"
#include "SVD.hpp"
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <iostream>
// Generic helper functions for any matrix size
template <uint8_t rows, uint8_t columns>
static float frobeniusNorm(const Matrix<rows, columns> &M) {
float sum = 0.0f;
for (int i = 0; i < rows; i++)
for (int j = 0; j < columns; j++) {
float v = M.Get(i, j);
sum += v * v;
}
return sqrtf(sum);
}
template <uint8_t n>
static bool isOrthogonal(const Matrix<n, n> &M, float tol = 1e-4f) {
Matrix<n, n> Mt = M.Transpose();
Matrix<n, n> MtM{0};
Mt.Mult(M, MtM);
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++) {
float expected = (i == j) ? 1.0f : 0.0f;
if (fabsf(MtM.Get(i, j) - expected) > tol)
return false;
}
return true;
}
TEST_CASE("SVD Integration: 2x2 [[1,2],[3,4]]", "[Matrix][SVD][Integration]") {
Matrix<2, 2> A{1, 2, 3, 4};
Matrix<2, 2> U{0};
Matrix<2, 1> sigma{0};
Matrix<2, 2> Vt{0};
SVD::SVD(A, U, sigma, Vt);
// Reference singular values from scipy: [5.464985704219, 0.365966190626]
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(5.4649857f, 1e-3f));
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(0.3659662f, 1e-3f));
// Check orthogonality of U and Vt (first 2x2 blocks)
REQUIRE(isOrthogonal<2>(U));
REQUIRE(isOrthogonal<2>(Vt));
// Check reconstruction: A ≈ U · diag(sigma) · Vt
Matrix<2, 2> recon{0};
Matrix<2, 2> Usig{0};
for (int i = 0; i < 2; i++)
for (int j = 0; j < 2; j++)
Usig[i][j] = U.Get(i, j) * sigma.Get(j, 0);
Usig.Mult(Vt, recon);
float err = 0.0f;
for (int i = 0; i < 2; i++)
for (int j = 0; j < 2; j++) {
float diff = recon.Get(i, j) - A.Get(i, j);
err += diff * diff;
}
err = sqrtf(err);
REQUIRE_THAT(err, Catch::Matchers::WithinRel(0.0f, 1e-3f));
std::cout << "SVD 2x2 [[1,2],[3,4]]:\n";
std::cout << "Sigma: [" << sigma.Get(0, 0) << ", " << sigma.Get(1, 0)
<< "]\n";
}
TEST_CASE("SVD Integration: 3x3 diagonal [10,5,2]",
"[Matrix][SVD][Integration]") {
Matrix<3, 3> A{10, 0, 0, 0, 5, 0, 0, 0, 2};
Matrix<3, 3> U{0};
Matrix<3, 1> sigma{0};
Matrix<3, 3> Vt{0};
SVD::SVD(A, U, sigma, Vt);
// Singular values should be [10, 5, 2] (already diagonal)
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(10.0f, 1e-3f));
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(5.0f, 1e-3f));
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(2.0f, 1e-3f));
// U and Vt should be identity (or close) for diagonal matrix
float uErr = frobeniusNorm(U - Matrix<3, 3>{1, 0, 0, 0, 1, 0, 0, 0, 1});
float vtErr = frobeniusNorm(Vt - Matrix<3, 3>{1, 0, 0, 0, 1, 0, 0, 0, 1});
REQUIRE_THAT(uErr, Catch::Matchers::WithinRel(0.0f, 1e-2f));
REQUIRE_THAT(vtErr, Catch::Matchers::WithinRel(0.0f, 1e-2f));
}
TEST_CASE("SVD Integration: 3x3 rank-deficient [[1,2,3],[4,5,6],[7,8,9]]",
"[Matrix][SVD][Integration]") {
Matrix<3, 3> A{1, 2, 3, 4, 5, 6, 7, 8, 9};
Matrix<3, 3> U{0};
Matrix<3, 1> sigma{0};
Matrix<3, 3> Vt{0};
SVD::SVD(A, U, sigma, Vt);
// Reference: [16.848103352614, 1.068369514555, 0.0]
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(16.8481f, 1e-2f));
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(1.06837f, 1e-2f));
// Third singular value should be ~0 (rank-deficient)
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-2f));
// Check reconstruction
Matrix<3, 3> recon{0};
Matrix<3, 3> Usig{0};
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
Usig[i][j] = U.Get(i, j) * sigma.Get(j, 0);
Usig.Mult(Vt, recon);
float err = 0.0f;
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++) {
float diff = recon.Get(i, j) - A.Get(i, j);
err += diff * diff;
}
err = sqrtf(err);
REQUIRE_THAT(err, Catch::Matchers::WithinRel(0.0f, 1e-2f));
std::cout << "SVD 3x3 rank-deficient:\n";
std::cout << "Sigma: [" << sigma.Get(0, 0) << ", " << sigma.Get(1, 0) << ", "
<< sigma.Get(2, 0) << "]\n";
}
TEST_CASE("SVD Integration: tall 4x3 matrix", "[Matrix][SVD][Integration]") {
Matrix<4, 3> A{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
Matrix<4, 3> U{0};
Matrix<3, 1> sigma{0};
Matrix<3, 3> Vt{0};
SVD::SVD(A, U, sigma, Vt);
// Reference: [25.462407436036, 1.290661675761, 0.0]
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(25.4624f, 1e-2f));
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(1.29066f, 1e-2f));
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-2f));
// Check reconstruction
Matrix<4, 3> recon{0};
Matrix<4, 3> Usig{0};
for (int i = 0; i < 4; i++)
for (int j = 0; j < 3; j++)
Usig[i][j] = U.Get(i, j) * sigma.Get(j, 0);
Usig.Mult(Vt, recon);
float err = 0.0f;
for (int i = 0; i < 4; i++)
for (int j = 0; j < 3; j++) {
float diff = recon.Get(i, j) - A.Get(i, j);
err += diff * diff;
}
err = sqrtf(err);
REQUIRE_THAT(err, Catch::Matchers::WithinRel(0.0f, 1e-2f));
std::cout << "SVD tall 4x3:\n";
std::cout << "Sigma: [" << sigma.Get(0, 0) << ", " << sigma.Get(1, 0) << ", "
<< sigma.Get(2, 0) << "]\n";
}
TEST_CASE("SVD Integration: wide 3x5 matrix", "[Matrix][SVD][Integration]") {
Matrix<3, 5> A{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
Matrix<3, 5> U{0};
Matrix<5, 1> sigma{0}; // sigma is columns x 1 = 5x1 for wide matrix
Matrix<5, 5> Vt{0}; // Vt is columns x columns = 5x5
SVD::SVD(A, U, sigma, Vt);
// Reference: [35.127223333575, 2.465396696917, 0.0]
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(35.1272f, 1e-2f));
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(2.46540f, 1e-2f));
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-2f));
// Check reconstruction: U (3x5) * diag(sigma) (5x3) = 3x3, then * Vt (3x5) =
// 3x5
Matrix<3, 5> recon{0};
Matrix<3, 5> Usig{0};
for (int i = 0; i < 3; i++)
for (int j = 0; j < 5; j++)
Usig[i][j] = U.Get(i, j) * sigma.Get(j, 0);
// For wide matrix: A = U * Sigma * Vt where U is 3x5, Sigma is 5x5
// (diagonal), Vt is 5x5 But our implementation returns sigma as 3x1 and Vt as
// 3x5 So we need: recon = Usig (3x5) * Vt (3x5)^T ... no that doesn't work
// either The SVD for wide matrices is: A = U * Sigma * Vt where:
// U is m×m (3×3), Sigma is m×n (3×5), Vt is n×n (5×5)
// But our API returns U as m×n (3×5), sigma as n×1 (3×1), Vt as n×n (3×5)
// So: recon = U (3x5) * diag(sigma) (5x5) * Vt (5x5)^T ...
// Actually, looking at the implementation, for wide matrices we swap roles.
// Let me just check reconstruction using the actual dimensions returned.
// For wide matrix: A (3x5) = U (3x5) * diag(sigma) (5x5 padded) * Vt (5x5)
// But our API returns Vt as 3x5, not 5x5
// The implementation stores: U = QR[:,0:p]^T (3x5), sigma (3x1), Vt =
// QL[:,0:p]^T (3x5) Reconstruction: A[i][j] = sum_k U[i][k]*sigma[k]*Vt[j][k]
float err2 = 0.0f;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 5; j++) {
float recon_val = 0.0f;
for (int k = 0; k < 3; k++) {
recon_val += U.Get(i, k) * sigma.Get(k, 0) * Vt.Get(j, k);
}
float diff = recon_val - A.Get(i, j);
err2 += diff * diff;
}
}
err2 = sqrtf(err2);
REQUIRE_THAT(err2, Catch::Matchers::WithinRel(0.0f, 1e-2f));
std::cout << "SVD wide 3x5:\n";
std::cout << "Sigma: [" << sigma.Get(0, 0) << ", " << sigma.Get(1, 0) << ", "
<< sigma.Get(2, 0) << "]\n";
}
TEST_CASE("SVD Integration: identity 3x3", "[Matrix][SVD][Integration]") {
Matrix<3, 3> A{1, 0, 0, 0, 1, 0, 0, 0, 1};
Matrix<3, 3> U{0};
Matrix<3, 1> sigma{0};
Matrix<3, 3> Vt{0};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(1.0f, 1e-3f));
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(1.0f, 1e-3f));
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(1.0f, 1e-3f));
float err = frobeniusNorm(U - Matrix<3, 3>{1, 0, 0, 0, 1, 0, 0, 0, 1});
REQUIRE_THAT(err, Catch::Matchers::WithinRel(0.0f, 1e-2f));
}
TEST_CASE("SVD Integration: symmetric positive definite 2x2 [[5,3],[3,5]]",
"[Matrix][SVD][Integration]") {
Matrix<2, 2> A{5, 3, 3, 5};
Matrix<2, 2> U{0};
Matrix<2, 1> sigma{0};
Matrix<2, 2> Vt{0};
SVD::SVD(A, U, sigma, Vt);
// For SPD matrix, singular values = eigenvalues: [8, 2]
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(8.0f, 1e-3f));
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(2.0f, 1e-3f));
// Check reconstruction
Matrix<2, 2> recon{0};
Matrix<2, 2> Usig{0};
for (int i = 0; i < 2; i++)
for (int j = 0; j < 2; j++)
Usig[i][j] = U.Get(i, j) * sigma.Get(j, 0);
Usig.Mult(Vt, recon);
float err = 0.0f;
for (int i = 0; i < 2; i++)
for (int j = 0; j < 2; j++) {
float diff = recon.Get(i, j) - A.Get(i, j);
err += diff * diff;
}
err = sqrtf(err);
REQUIRE_THAT(err, Catch::Matchers::WithinRel(0.0f, 1e-3f));
std::cout << "SVD SPD 2x2 [[5,3],[3,5]]:\n";
std::cout << "Sigma: [" << sigma.Get(0, 0) << ", " << sigma.Get(1, 0)
<< "]\n";
}
+482
View File
@@ -0,0 +1,482 @@
#!/usr/bin/env python3
"""
Generate reference values for SVD building block unit tests.
Run this to verify/implement the C++ SVD implementation against scipy/numpy.
Usage: python3 svd-reference-values.py
"""
import numpy as np
from scipy.linalg import svd, qr as scipy_qr
import json
def compute_householder(x):
"""Compute Householder reflector: H*x = [alpha, 0, 0, ...]^T.
Returns (v_normalized, alpha) where v is the normalized Householder vector.
H = I - 2*v*v^T / (v^T*v)
"""
x = np.array(x, dtype=np.float64)
norm_x = np.linalg.norm(x)
if norm_x < 1e-30:
return x.copy(), 0.0
alpha = -np.sign(x[0]) * norm_x if x[0] != 0 else -norm_x
v = x.copy()
v[0] -= alpha
v_norm = np.linalg.norm(v)
if v_norm < 1e-30:
return np.zeros_like(x), alpha
v /= v_norm
return v, alpha
def apply_householder_left(A, v, start_row):
"""Apply Householder reflection from the left: A = (I - 2vv^T) @ A.
v is the normalized Householder vector operating on rows [start_row:].
The length of v must match the number of rows affected.
"""
A = A.copy()
k = len(v)
for col in range(A.shape[1]):
dot = np.dot(v, A[start_row:start_row+k, col])
A[start_row:start_row+k, col] -= 2.0 * dot * v
return A
def apply_householder_right(A, v, start_col):
"""Apply Householder reflection from the right: A = A @ (I - 2vv^T).
v is the normalized Householder vector operating on columns [start_col:].
The length of v must match the number of columns affected.
"""
A = A.copy()
k = len(v)
for row in range(A.shape[0]):
dot = np.dot(A[row, start_col:start_col+k], v)
A[row, start_col:start_col+k] -= 2.0 * dot * v
return A
def compute_givens(x, y):
"""Compute Givens rotation that zeros out y.
Returns (c, s) such that [c s; -s c] @ [x; y] = [r; 0].
"""
r = np.sqrt(x*x + y*y)
if r < 1e-30:
return 1.0, 0.0
c = x / r
s = y / r
return c, s
def apply_givens_left(A, i, j, c, s):
"""Apply Givens rotation from the left to rows i and j of A.
[c s] [row_i]
[-s c] @ [row_j] = [new_row_i]
[new_row_j]
"""
A = A.copy()
new_i = c * A[i] + s * A[j]
new_j = -s * A[i] + c * A[j]
A[i] = new_i
A[j] = new_j
return A
def apply_givens_right(A, i, j, c, s):
"""Apply Givens rotation from the right to columns i and j of A.
[col_i col_j] @ [c -s] = [new_col_i new_col_j]
[s c]
"""
A = A.copy()
new_i = c * A[:, i] + s * A[:, j]
new_j = -s * A[:, i] + c * A[:, j]
A[:, i] = new_i
A[:, j] = new_j
return A
def householder_bidiagonalization(A):
"""Full Householder bidiagonalization: A = Q_L @ B @ Q_R^T.
Returns (B, Q_L, Q_R) where B is upper bidiagonal.
"""
m, n = A.shape
p = min(m, n)
QL = np.eye(m, dtype=np.float64)
QR = np.eye(n, dtype=np.float64)
W = A.copy()
for k in range(p):
# Left HH: zero out W[k+1:, k]
if k < m - 1:
x = W[k+1:, k].copy()
v, alpha = compute_householder(x)
if np.linalg.norm(v) > 1e-30:
W = apply_householder_left(W, v, k + 1)
QL = apply_householder_right(QL, v, k + 1)
# Right HH: zero out W[k, k+2:] (superdiagonal)
if k < p - 1 and k + 2 <= n:
x = W[k, k+2:].copy()
v, alpha = compute_householder(x)
if np.linalg.norm(v) > 1e-30:
W = apply_householder_right(W, v, k + 2)
QR = apply_householder_right(QR, v, k + 2)
return W, QL, QR
def implicit_qr_iteration(B, QR_acc):
"""Implicit QR iteration on a bidiagonal matrix.
Returns (Sigma, QR_acc) where Sigma is diagonal with singular values
and QR_acc contains the accumulated right transformations.
"""
m, n = B.shape
p = min(m, n)
W = B.copy()
max_iter = 1000
tol = 1e-10
for iteration in range(max_iter):
# Deflate negligible subdiagonal elements
for i in range(p - 1, 0, -1):
if abs(W[i, i-1]) < tol * (abs(W[i-1, i-1]) + abs(W[i, i])):
W[i, i-1] = 0.0
# Find smallest unreduced block [start, end]
start = 0
for i in range(p - 1):
if abs(W[i+1, i]) >= tol * (abs(W[i, i]) + abs(W[i+1, i+1])):
start = i + 1
end = p - 1
for i in range(p - 2, -1, -1):
if abs(W[i+1, i]) >= tol * (abs(W[i, i]) + abs(W[i+1, i+1])):
end = i
break
if start >= end:
continue
# Wilkinson shift from bottom 2x2 corner
a, b = W[end-1, end-1], W[end-1, end]
c_val, d = W[end, end-1], W[end, end]
trace = a + d
det = a * d - b * c_val
disc = trace**2 - 4 * det
if disc >= 0:
sqrt_disc = np.sqrt(disc)
e1, e2 = (trace + sqrt_disc) / 2, (trace - sqrt_disc) / 2
shift = e1 if abs(e1 - d) < abs(e2 - d) else e2
else:
shift = d
# Implicit QR step using Givens rotations
# Process from top to bottom within the block
x = W[start, start] - shift
y = W[start + 1, start]
for i in range(start, end):
r = np.sqrt(x*x + y*y)
if r < 1e-30:
x = W[i + 1, i]
y = W[i + 1, i + 1] if i + 2 <= end else 0.0
continue
c_rot = x / r
s_rot = y / r
# Apply from left to rows i, i+1 (columns i..n-1)
for j in range(i, n):
t1, t2 = W[i, j], W[i + 1, j]
W[i, j] = c_rot * t1 + s_rot * t2
W[i + 1, j] = -s_rot * t1 + c_rot * t2
# Apply from right to columns i, i+1 (rows 0..i)
if i > start:
for j in range(i + 1):
t1, t2 = W[j, i], W[j, i + 1]
W[j, i] = c_rot * t1 + s_rot * t2
W[j, i + 1] = -s_rot * t1 + c_rot * t2
# Accumulate into QR_acc
for j in range(QR_acc.shape[0]):
t1, t2 = QR_acc[j, i], QR_acc[j, i + 1]
QR_acc[j, i] = c_rot * t1 + s_rot * t2
QR_acc[j, i + 1] = -s_rot * t1 + c_rot * t2
# Prepare for next rotation
x = W[i + 1, i]
y = W[i + 1, i + 1] if i + 2 <= end else 0.0
return W, QR_acc
def main():
print("=" * 70)
print("SVB BUILDING BLOCK REFERENCE VALUES")
print("Generated with scipy/numpy for C++ unit test verification")
print("=" * 70)
# ------------------------------------------------------------------
# Test 1: Householder Vector Computation
# ------------------------------------------------------------------
print("\n" + "=" * 70)
print("TEST 1: computeHouseholderVector")
print("=" * 70)
test_vectors = [
("2D [1,3]", [1.0, 3.0]),
("2D [3,4] (norm=5)", [3.0, 4.0]),
("3D [1,2,3]", [1.0, 2.0, 3.0]),
("3D [0,0,1]", [0.0, 0.0, 1.0]),
("4D [5,-3,2,1]", [5.0, -3.0, 2.0, 1.0]),
]
for name, vec in test_vectors:
v, alpha = compute_householder(vec)
x = np.array(vec)
Hx = x - 2 * np.dot(v, x) * v
print(f"\n{name}:")
print(f" Input: {list(x)}")
print(f" ||x||: {np.linalg.norm(x):.15f}")
print(f" alpha: {alpha:.15f}")
print(f" v (normalized): {[round(float(vi), 12) for vi in v]}")
print(f" H*x = [alpha,0..]: {[round(float(xi), 12) for xi in Hx]}")
print(f" Off-diagonal ~0: {np.allclose(Hx[1:], 0, atol=1e-12)}")
# ------------------------------------------------------------------
# Test 2: Householder Apply Left
# ------------------------------------------------------------------
print("\n" + "=" * 70)
print("TEST 2: applyHouseholderLeft")
print("=" * 70)
A_test = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]], dtype=np.float64)
x_col = A_test[1:, 0].copy()
v_left, _ = compute_householder(x_col)
print(f"\nInput matrix:\n{A_test}")
print(f"Householder vector (rows 1:3): {[round(float(vi), 12) for vi in v_left]}")
A_result = apply_householder_left(A_test, v_left, 1)
print(f"\nAfter applyHouseholderLeft:\n{A_result}")
print(f" A[1,0] = {A_result[1,0]:.2e}, A[2,0] = {A_result[2,0]:.2e} (should be ~0)")
# ------------------------------------------------------------------
# Test 3: Householder Apply Right
# ------------------------------------------------------------------
print("\n" + "=" * 70)
print("TEST 3: applyHouseholderRight")
print("=" * 70)
A_test = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]], dtype=np.float64)
x_row = A_test[0, 1:].copy()
v_right, _ = compute_householder(x_row)
print(f"\nInput matrix:\n{A_test}")
print(f"Householder vector (cols 1:3): {[round(float(vi), 12) for vi in v_right]}")
A_result = apply_householder_right(A_test, v_right, 1)
print(f"\nAfter applyHouseholderRight:\n{A_result}")
print(f" A[0,1] = {A_result[0,1]:.2e}, A[0,2] = {A_result[0,2]:.2e} (should be ~0)")
# ------------------------------------------------------------------
# Test 4: Givens Rotation Computation
# ------------------------------------------------------------------
print("\n" + "=" * 70)
print("TEST 4: computeGivens")
print("=" * 70)
givens_tests = [
("3-4-5 triangle", 3.0, 4.0),
("y already zero", 1.0, 0.0),
("x is zero", 0.0, 5.0),
("Both negative", -3.0, -4.0),
("45 degree case", 1.0, -1.0),
]
for name, x, y in givens_tests:
c, s = compute_givens(x, y)
result_x = c * x + s * y
result_y = -s * x + c * y
print(f"\n{name}: x={x}, y={y}")
print(f" r = {np.sqrt(x*x+y*y):.12f}")
print(f" c = {c:.12f}, s = {s:.12f}")
print(f" [c s; -s c] @ [x;y] = [{result_x:.2e}, {result_y:.2e}]")
# ------------------------------------------------------------------
# Test 5: Apply Givens Left/Right
# ------------------------------------------------------------------
print("\n" + "=" * 70)
print("TEST 5: applyGivensLeft / applyGivensRight")
print("=" * 70)
A_test = np.array([[3.0, 4.0], [1.0, 2.0]], dtype=np.float64)
c, s = compute_givens(3.0, 1.0)
print(f"\nInput matrix:\n{A_test}")
print(f"Givens rotation (rows 0,1): c={c:.12f}, s={s:.12f}")
A_left = apply_givens_left(A_test, 0, 1, c, s)
print(f"\nAfter applyGivensLeft:\n{A_left}")
print(f" A[1,0] = {A_left[1,0]:.2e} (should be ~0)")
A_test = np.array([[3.0, 1.0], [4.0, 2.0]], dtype=np.float64)
c, s = compute_givens(3.0, 4.0)
print(f"\nInput matrix:\n{A_test}")
print(f"Givens rotation (cols 0,1): c={c:.12f}, s={s:.12f}")
A_right = apply_givens_right(A_test, 0, 1, c, s)
print(f"\nAfter applyGivensRight:\n{A_right}")
print(f" A[0,1] = {A_right[0,1]:.2e} (should be ~0)")
# ------------------------------------------------------------------
# Test 6: Full Bidiagonalization
# ------------------------------------------------------------------
print("\n" + "=" * 70)
print("TEST 6: householderBidiagonalization")
print("=" * 70)
bidiag_tests = [
("2x2 [[1,2],[3,4]]", np.array([[1.0, 2.0], [3.0, 4.0]])),
("3x3 SPD [[5,3],[3,5]]", np.array([[5.0, 3.0], [3.0, 5.0]])),
("3x3 diag [[10,0,0],[0,5,0],[0,0,2]]",
np.array([[10.0, 0, 0], [0, 5.0, 0], [0, 0, 2.0]])),
("3x3 full [[1,2,3],[4,5,6],[7,8,10]]",
np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 10.0]])),
("Tall 4x3", np.array([[1,2,3],[4,5,6],[7,8,9],[10,11,12]], dtype=np.float64)),
]
for name, A in bidiag_tests:
B, QL, QR = householder_bidiagonalization(A)
m, n = A.shape
p = min(m, n)
print(f"\n{name}:")
print(f" Original:\n{A}")
print(f"\n Bidiagonal B:\n{B}")
print(f" Diagonal: {[round(float(B[i,i]), 10) for i in range(p)]}")
print(f" Superdiag: {[round(float(B[i,i+1]), 10) for i in range(min(p-1, n-1))]}")
recon = QL @ B @ QR.T
err = np.linalg.norm(recon - A, 'fro')
print(f" ||QL @ B @ QR^T - A||_F = {err:.2e}")
# ------------------------------------------------------------------
# Test 7: Full SVD Reference Values
# ------------------------------------------------------------------
print("\n" + "=" * 70)
print("TEST 7: Full SVD Reference Values (scipy.linalg.svd)")
print("=" * 70)
test_matrices = [
("Simple 2x2", np.array([[1,2],[3,4]], dtype=np.float64)),
("SPD 2x2", np.array([[5,3],[3,5]], dtype=np.float64)),
("Full-rank 3x3", np.array([[1,2,3],[4,5,6],[7,8,10]], dtype=np.float64)),
("Rank-deficient 3x3", np.array([[1,2,3],[4,5,6],[7,8,9]], dtype=np.float64)),
("Diagonal 3x3", np.array([[10,0,0],[0,5,0],[0,0,2]], dtype=np.float64)),
("Tall 4x3", np.array([[1,2,3],[4,5,6],[7,8,9],[10,11,12]], dtype=np.float64)),
("Wide 3x5", np.array([[1,2,3,4,5],[6,7,8,9,10],[11,12,13,14,15]], dtype=np.float64)),
("Symmetric tri 5x5", np.array([[2,-1,0,0,0],[-1,2,-1,0,0],[0,-1,2,-1,0],[0,0,-1,2,-1],[0,0,0,-1,2]], dtype=np.float64)),
("Neg values 2x3", np.array([[0.5,-0.3,0.8],[-0.2,0.7,0.1]], dtype=np.float64)),
("Near-singular 2x2", np.array([[1,0],[0,1e-6]], dtype=np.float64)),
("Orthogonal 3x3", np.array([[np.cos(np.pi/4), -np.sin(np.pi/4), 0],
[np.sin(np.pi/4), np.cos(np.pi/4), 0],
[0, 0, 1]], dtype=np.float64)),
("Identity 3x3", np.eye(3)),
("Zero 3x3", np.zeros((3,3))),
("Col vector 2x1", np.array([[3],[4]], dtype=np.float64)),
("Row vector 1x2", np.array([[3,4]], dtype=np.float64)),
]
for name, A in test_matrices:
U, s, Vt = svd(A, full_matrices=False)
print(f"\n{name}: shape={A.shape}")
print(f" Singular values: {[round(float(x), 12) for x in s]}")
print(f" U:\n{np.array2string(U, precision=6, floatmode='maxprec_equal')}")
print(f" Vt:\n{np.array2string(Vt, precision=6, floatmode='maxprec_equal')}")
recon_err = np.linalg.norm(A - U @ np.diag(s) @ Vt, 'fro')
print(f" Reconstruction error: {recon_err:.2e}")
# ------------------------------------------------------------------
# Test 8: Implicit QR Iteration on Bidiagonal
# ------------------------------------------------------------------
print("\n" + "=" * 70)
print("TEST 8: implicitQRIteration")
print("=" * 70)
qr_tests = [
("2x2 [[1,2],[3,4]]", np.array([[1.0, 2.0], [3.0, 4.0]])),
("3x3 diag", np.array([[10.0, 0, 0], [0, 5.0, 0], [0, 0, 2.0]])),
]
for name, A in qr_tests:
B, QL, QR = householder_bidiagonalization(A)
Sigma, QR_final = implicit_qr_iteration(B.copy(), QR.copy())
print(f"\n{name}:")
print(f" Bidiagonal B:\n{B}")
print(f" After QR iteration (Sigma):\n{Sigma}")
print(f" Diagonal entries: {[round(float(Sigma[i,i]), 10) for i in range(min(Sigma.shape))]}")
# Verify: QL @ Sigma @ QR_final^T ≈ A
recon = QL @ Sigma @ QR_final.T
err = np.linalg.norm(recon - A, 'fro')
print(f" ||QL @ Sigma @ QR^T - A||_F = {err:.2e}")
# ------------------------------------------------------------------
# JSON output for easy import into C++ tests
# ------------------------------------------------------------------
print("\n" + "=" * 70)
print("JSON OUTPUT (for easy C++ integration)")
print("=" * 70)
json_data = {}
# Householder test vectors
hh_tests = {}
for name, vec in test_vectors:
v, alpha = compute_householder(vec)
x = np.array(vec)
Hx = x - 2 * np.dot(v, x) * v
hh_tests[name] = {
"input": [float(xi) for xi in x],
"norm": float(np.linalg.norm(x)),
"alpha": float(alpha),
"v_normalized": [round(float(vi), 12) for vi in v],
"Hx": [round(float(xi), 12) for xi in Hx],
}
json_data["householder_vectors"] = hh_tests
# Full SVD reference values
svd_tests = {}
for name, A in test_matrices:
U, s, Vt = svd(A, full_matrices=False)
svd_tests[name] = {
"shape": list(A.shape),
"singular_values": [round(float(x), 12) for x in s],
"U": [[round(float(U[i,j]), 8) for j in range(U.shape[1])] for i in range(U.shape[0])],
"Vt": [[round(float(Vt[i,j]), 8) for j in range(Vt.shape[1])] for i in range(Vt.shape[0])],
}
json_data["svd_reference"] = svd_tests
print(json.dumps(json_data, indent=2))
if __name__ == "__main__":
main()
@@ -0,0 +1,36 @@
Running matrix-timing-tests with timing
Randomness seeded to: 3567651885
1.857 s: Addition
1.857 s: Timing Tests
1.788 s: Subtraction
1.788 s: Timing Tests
1.929 s: Multiplication
1.929 s: Timing Tests
1.268 s: Scalar Multiplication
1.268 s: Timing Tests
1.798 s: Element Multiply
1.798 s: Timing Tests
1.802 s: Element Divide
1.803 s: Timing Tests
1.553 s: Minor Matrix
1.554 s: Timing Tests
1.009 s: Determinant
1.009 s: Timing Tests
4.076 s: Matrix of Minors
4.076 s: Timing Tests
1.066 s: Invert
1.066 s: Timing Tests
1.246 s: Transpose
1.246 s: Timing Tests
2.284 s: Normalize
2.284 s: Timing Tests
0.606 s: GET ROW
0.606 s: Timing Tests
24.629 s: GET COLUMN
24.630 s: Timing Tests
3.064 s: QR Decomposition
3.064 s: Timing Tests
===============================================================================
test cases: 1 | 1 passed
assertions: - none -
+45
View File
@@ -0,0 +1,45 @@
// include the unit test framework first
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
// include the module you're going to test next
#include "Vector3D.hpp"
#include "Matrix.hpp"
// any other libraries
#include <array>
#include <cmath>
#include <iostream>
TEST_CASE("Vector Math", "Vector")
{
V3D<float> v1{1, 2, 3};
V3D<float> v2{4, 5, 6};
V3D<float> v3{};
SECTION("Initialization")
{
// list initialization
REQUIRE(v1.x == 1);
REQUIRE(v1.y == 2);
REQUIRE(v1.z == 3);
// copy initialization
V3D<float> v4{v2};
REQUIRE(v4.x == 4);
REQUIRE(v4.y == 5);
REQUIRE(v4.z == 6);
// empty initialization
REQUIRE(v3.x == 0);
REQUIRE(v3.y == 0);
REQUIRE(v3.z == 0);
// matrix initialization
Matrix<1, 3> mat1{v1.ToArray()};
V3D<float> v5{mat1};
REQUIRE(v5.x == v1.x);
REQUIRE(v5.y == v1.y);
REQUIRE(v5.z == v1.z);
}
}