diff --git a/Project.toml b/Project.toml index 86ea0a5..2e084bf 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,17 @@ name = "RidgeRegression" uuid = "739161c8-60e1-4c49-8f89-ff30998444b1" -authors = ["Vivak Patel "] version = "0.1.0" +authors = ["Eton Tackett ", "Vivak Patel "] + +[deps] +CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" [compat] +CSV = "0.10.15" +DataFrames = "1.8.1" +Downloads = "1.7.0" +LinearAlgebra = "1.12.0" julia = "1.12.4" diff --git a/docs/make.jl b/docs/make.jl index a1097bb..d42cfbe 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -14,6 +14,7 @@ makedocs(; ), pages=[ "Home" => "index.md", + "Design" => "design.md", ], ) diff --git a/docs/src/design.md b/docs/src/design.md new file mode 100644 index 0000000..db8541e --- /dev/null +++ b/docs/src/design.md @@ -0,0 +1,182 @@ +# Motivation and Background +Many modern science problems involve regression problems with extremely large numbers of +predictors. +[Genome-wide association studies (GWAS)](https://doi.org/10.1371/journal.pone.0245376), +for example, try to identify genetic +variants associated with a disease phenotype using hundreds of thousands or millions of +genomic features. In such settings, traditional least squares methods fail. +Penalized Least Squares (PLS) extends ordinary least squares (OLS) +regression by adding a penalty term to shrink parameter estimates. Ridge regression, an +approach within PLS, adds a regularization term, producing a regularized estimator. + +Mathematically, ridge regression estimates the regression coefficients by solving the +penalized least squares problem +```math +\hat{\boldsymbol{\beta}} = +\arg\min_{\boldsymbol{\beta}} +\left( +\| \mathbf{y} - X\boldsymbol{\beta} \|_2^2 ++ +\lambda \| \boldsymbol{\beta} \|_2^2 +\right) +``` +where $\lambda > 0$ is a regularization parameter that controls the strength of the penalty. + +The purpose of ridge regression is to stabilize regression estimates when the predictors are +highly correlated or the design matrix $X$ is nearly singular. Ridge regression modifies the +least squares objective by adding a penalty on the squared $\ell_2$-norm of the coefficient +vector. The estimator is obtained by minimizing a penalized least squares objective in which +large coefficient values are discouraged through the penalty term $\lambda +\|\boldsymbol{\beta}\|_2^2$. This penalty shrinks the estimated coefficients toward the +origin, which reduces the variance of the estimator and mitigates the effects of +multicollinearity. + +There are many numerical algorithms available to compute ridge regression estimates +including direct methods, Krylov subspace methods, gradient-based optimization, coordinate +descent, and stochastic gradient descent. These algorithms differ in their computational +costs and numerical stability. + +The goal of this experiment is to investigate the performance of these algorithms when we +vary the structure and scale of the regression problem. To do this, we consider the linear +model $\mathbf{y} = X\boldsymbol{\beta} + \boldsymbol{\varepsilon}$ where the matrix ${X}$ +may be constructed with varying dimensions, sparsity patterns, and conditioning properties. +# Questions +The primary goal of this experiment is to compare numerical algorithms for computing ridge +regression estimates under various conditions. In particular, we aim to address the +following questions: + +1. How does the performance of ridge regression algorithms change as the structural + and numerical properties of the regression problem vary? + +2. Which ridge regression algorithm provides the best balance between numerical + stability and computational cost across these problem regimes? + +# Experimental Units +An experimental unit is defined by a triplet: a design matrix, $X$; a response vector, $y$; +and a non-negative regularization parameter $\lambda$. + +Datasets will be generated either fully artificially or semi-artificially. In the artifical setting, both the design matrix (X) and the response vector (y) are generated. In the semi-artifical setting, the design matrix is fixed from a real dataset (e.g. GWAS) and the response vector is generated conditional on X. This is done so that we can compute solution error. + +Experimental units are selected based on factors we believe will potentially impact +the performance of a specific algorithm. +These factors, or blocks, correspond to: +- The dimensional regime of $X \in \mathbb{R}^{n \times p}$: + $p \ll n$, $p ≈ n$, and $p \gg n$. +- The magnitude of the regularization parameter, $\lambda$, as described below. + +Matrix sparsity is a factor that can influence algorithm performance. However, we will not consider it in this experiment because none of the algorithms under consideration are implemented in a manner that exploits sparse matrix structure. + +The regularization parameter is often selected using one of the following strategies, +depending on application domain: + +- [Generalized Cross Validation](https://doi.org/10.1080/00401706.1979.10489751) +- [The L-Curve Method](https://doi.org/10.1137/1034115) +- [Information Criteria](https://doi.org/10.1002/wics.1607) +- [Morozov's Discrepancy Principle](https://doi.org/10.1088/0266-5611/26/2/025001) + +Excluding Morozov's discrepancy principle, the remaining methods require +computing $\hat{\beta}_\lambda = (X^\top X + \lambda I)^{-1}X^\top y$ or +$\Vert X\hat{\beta}_\lambda - y \Vert_2$ over a grid of values for $\lambda$. +Given our assumption that problem conditioning will impact algorithm behavior, +we will choose this grid to modify the problem's condition number systematically. + +Recall, the ridge estimator's closed form is given by solving +```math +\min_{\beta} \left\Vert \begin{bmatrix} X \\ \lambda I \end{bmatrix}\beta - +y \right\Vert_2^2, +``` +or (equivalently, from a mathematical perspective) solving +$(X^\top X + \lambda I)\hat{\beta}_\lambda = X^\top y$. +Let $\sigma_{\max}$ denote the largest singular value +of $X$, and let $\sigma_{\min}$ denote the smallest singular value of +$X$ (note, $0$ is allowed). +Then the condition number for the normal systems approach is +```math +\kappa(X^\top X+\lambda I) += +\frac{\sigma_{\max}^2+\lambda}{\sigma_{\min}^2+\lambda}. +``` + +There are two cases we consider. +- Case 1: $\sigma_{\min}=0$. In this case, the least squares problem is ill-posed. We choose + the smallest value of $\lambda$ in the grid to be + $\sigma_{\max}^2 / (10^{12} - 1)$, which ensures + $\kappa(X^\top X + \lambda I) = 10^{12}$. +- Case 2: $\sigma_{\min}>0$. In this case, we choose the smallest value of $\lambda$ in the + grid to be $\sigma_{\max}\sigma_{\min}$, which ensures + $\kappa(X^\top X + \lambda I) = \sigma_{\max}/\sigma_{\min}$. + That is, it sets the regularized normal system's condition number to that of the original + least squares problem. + +In both cases, assuming $\sigma_{\max}^2 > 2 \sigma_{\min}$, we choose the larges value of +$\lambda$ to be $\sigma_{\max}^2 - 2 \sigma_{\min}$. The condition number for the +regularized normal equations is $\kappa(X^\top X + \lambda I)=2$. + +For each experimental unit, a grid of 15 regularization parameters will be constructed between $\lambda _{min}$ and $\lambda _{max}$. The endpoints of the grid are chosen to correspond to the range of condition numbers of the normal equations. Intermediate values will be selected using a log-scale. We will use logarithmic scaling because $\kappa(X^\top X+\lambda I)$ changes rapidly as $\lambda \rightarrow 0$, making linear spacing insufficient for capturing performance. + +Suppose we have m values, $\lambda _1\:,...,\:\lambda _m$ where $\lambda _1=\lambda_{min}$ and $\lambda _m=\lambda _{max}$. Logarithmic scaling tells us we want the ratio between these values to be constant. To ensure this, we consider a geometric series of the form: +```math +\lambda _i=\lambda _{min}\left(r\right)^{i-1}, i = 1, 2, ..., 15 +``` +Our goal here is to derive the common ratio r. Relating $\lambda _{min}$ and $\lambda _{max}$ gives us +```math +\lambda _{max}=\lambda _{min}\left(r\right)^{m-1} +``` +Using algebra to isolate r +```math +\frac{\lambda_{\text{max}}}{\lambda_{\text{min}}} = r^{m-1} +``` +Applying logarithms and rearranging we obtain +```math +r=e^{\frac{log\left(\frac{\lambda _{max}}{\lambda _{min}}\right)}{m-1}}=\left(\frac{\lambda _{max}}{\lambda _{min}}\right)^{\frac{1}{m-1}} +``` +In our case $m=15$ + +The total number of block combinations is determined by the product of the number of levels +in each factor, denoted b. We will also denote r to be the number of replicated +datasets in each block. Here, we mean the number datasets within a block. The total number +of experimental units is then ${b * r}$. + + +| Blocking System | Factor | Blocks | +|:----------------|:-------|:-------| +| Dataset | Dimensional regime | $(p \ll n)$, $(p \approx n)$, $(p \gg n)$| +| Conditioning | Condition number of $X^\top X + \lambda I$ | 15 logartihmically spaced $\lambda$ values so that $\kappa(X^\top X+\lambda I)$ ranges from poorly conditioned to conditioned. | + +# Treatments + +The treatments are the ridge regression solution methods: + +- Gradient-based optimization +- Stochastic gradient descent +- Direct Methods + - Golub Kahan Bidiagonalization + + Since each experimental unit will recieves all t treatments, the total number of algorithm + runs in the experiment is ${t * b * r}$. For this experiment, ${t=3}$. To ensure fair + comparison between algorithms, each treatment will be applied under a fixed wall time + constraint. Each algorithm will be run for a maximum of two hours per experimental unit. +# Observational Units and Measurements + +The observational units are each algorithm-dataset pair. For each combination we will observe the following + +| Column Name | Data Type | Description | +|:---|:---|:---| +| `dataset_id` | Positive Integer | Identifier for the generated dataset (experimental unit). | +| `dimensional_regime` | String | Relationship between predictors and observations: `p << n`, `p ≈ n`, or `p >> n`. | +| `lambda` | Positive Floating-point| Regularization parameter used. | +| `condition_number` | Positive Floating-point| $\kappa(X^\top X+\lambda I)$ corresponding to the selected $\lambda$ | +| `algorithm` | String | Ridge regression solution method used: `GradientDescent`, `SGD`, or `DirectMethod`. | +| `runtime_seconds` | Positive Floating-point | Time required for the algorithm to compute a solution. | +| `iterations` | Positive Integer | Number of iterations performed by the algorithm (`NA` for direct methods). | +| `step_size` | Positive Floating-point | Step size used in gradient descent or SGD (`NA` for direct methods). | +| `batch_size` | Positive Integer | Number of samples used per SGD update (`NA` for direct methods and gradient descent). | +| `number_of_epochs` | Positive Integer | Number epochs per observation (`NA` for direct methods). | + + + +The collected measurements will be written to a CSV file. Each row in the file corresponds +to a single algorithm–dataset pair, which forms the observational unit of the experiment. +The columns represent the recorded measurements. After the experiment, the resulting CSV +file should contain ${Algorithms∗Datasets}$ number of rows and each row will contain exactly +10 columns. \ No newline at end of file diff --git a/src/RidgeRegression.jl b/src/RidgeRegression.jl index c32de91..f582b2c 100644 --- a/src/RidgeRegression.jl +++ b/src/RidgeRegression.jl @@ -1,5 +1,11 @@ module RidgeRegression -# Write your package code here. +using CSV +using DataFrames +using LinearAlgebra + +include("bidiagonalization.jl") + +export compute_givens, rotate_rows!, rotate_cols!, bidiagonalize_with_H, apply_Ht_to_b, bidiagonalize_A end diff --git a/src/bidiagonalization.jl b/src/bidiagonalization.jl new file mode 100644 index 0000000..7f379ac --- /dev/null +++ b/src/bidiagonalization.jl @@ -0,0 +1,189 @@ +""" + compute_givens(a, b) + +Compute Givens rotation coefficients for scalars `a` and `b`. + +# Arguments +- `a::Number`: First scalar +- `b::Number`: Second scalar + +# Returns +- `c::Number`: Cosine coefficient of the Givens rotation +- `s::Number`: Sine coefficient of the Givens rotation + +""" +function compute_givens(a::Number, b::Number) # Compute Givens rotation coefficients for scalars a and b + if b == 0 + return one(a), zero(a) + elseif a == 0 + throw(ArgumentError("a is zero, cannot compute Givens rotation")) + else + r = hypot(a, b) + c = a / r + s = b / r + return c, s + end +end + +""" + rotate_rows!(M::AbstractMatrix,i::Int,j::Int,c::Number,s::Number) + +Apply a Givens rotation to rows `i` and `j` of matrix `M`. + +# Arguments +- `M::AbstractMatrix`: The matrix to be rotated +- `i::Int`: First row index +- `j::Int`: Second row index +- `c::Number`: Cosine coefficient of the Givens rotation +- `s::Number`: Sine coefficient of the Givens rotation + +# Returns +- `M::AbstractMatrix`: The rotated matrix + +""" +function rotate_rows!(M::AbstractMatrix,i::Int,j::Int,c::Number,s::Number) + for k in 1:size(M,2) # Loop over columns + temp = M[i,k] # Store the original value of M[i,k] before modification + M[i,k] = c*temp + s*M[j,k] + M[j,k] = -conj(s)*temp + c*M[j,k] #Apply the Givens rotation to the elements in rows i and j + end + return M +end + + +""" + rotate_cols!(M::AbstractMatrix,i::Int,j::Int,c::Number,s::Number) + +Apply a Givens rotation to columns `i` and `j` of matrix `M`. + +# Arguments +- `M::AbstractMatrix`: The matrix to be rotated +- `i::Int`: First column index +- `j::Int`: Second column index +- `c::Number`: Cosine coefficient of the Givens rotation +- `s::Number`: Sine coefficient of the Givens rotation + +# Returns +- `M::AbstractMatrix`: The rotated matrix + +""" +function rotate_cols!(M::AbstractMatrix,i::Int,j::Int,c::Number,s::Number) + for k in 1:size(M,1) # Loop over rows + temp = M[k,i] # Store the original value of M[k,i] before modification + M[k,i] = c*temp + s*M[k,j] + M[k,j] = -conj(s)*temp + c*M[k,j] # Apply the Givens rotation to the elements in columns i and j + end + return M +end +""" + bidiagonalize_A(A::AbstractMatrix, L::AbstractMatrix, b::AbstractVector) + +Performs bidiagonalization of a matrix A using a sequence of Givens transformations while explicitly accumulating +the orthogonal left factor `H` and right factor `K` such that + + H' * A * K = B. + +# Arguments +- `A::AbstractMatrix`: The matrix to be bidiagonalized +- `L::AbstractMatrix`: The banded matrix to be updated in-place + +# Returns +- `B::AbstractMatrix`: The bidiagonalized form of the input matrix A with dimension (n,n) +- `C::AbstractMatrix`: The matrix resulting from the sequence of Givens transformations +- `H::AbstractMatrix`: The orthogonal left factor +- `K::AbstractMatrix`: The orthogonal right factor +- `Ht::AbstractMatrix`: The transpose of the orthogonal left factor + +""" + +function bidiagonalize_A(A::AbstractMatrix, L::AbstractMatrix) + m, n = size(A) + + B = copy(A) #Will be transformed into bidiagonal form + C = copy(L) + + Ht = Matrix{eltype(A)}(I, m, m) #Ht will accumulate the left transformations, initialized as identity + K = Matrix{eltype(A)}(I, n, n) #K will accumulate the right transformations, initialized as identity + + imax = min(m, n) + + for i in 1:imax + # Left Givens rotations: zero below diagonal in column i + for j in m:-1:(i + 1) + if B[j, i] != 0 + c, s = compute_givens(B[i, i], B[j, i]) #Build Givens rotation to zero B[j, i] + rotate_rows!(B, i, j, c, s) #Apply the Givens rotation to rows i and j of B + rotate_rows!(Ht, i, j, c, s) #Accumulate the left transformations in Ht + B[j, i] = zero(eltype(B)) + end + end + + # Right Givens rotations: zero entries right of superdiagonal + if i <= n - 2 + for k in n:-1:(i + 2) + if B[i, k] != 0 + c, s = compute_givens(B[i, i + 1], B[i, k]) #Build Givens rotation to zero B[i, k] + #s = -s + rotate_cols!(B, i + 1, k, c, s) #Apply the Givens rotation to columns i+1 and k of B + rotate_cols!(C, i + 1, k, c, s) #Apply the same right rotation to C, since C is updated by the right transformations + rotate_cols!(K, i + 1, k, c, s) #Accumulate the right transformations in K + B[i, k] = zero(eltype(B)) + end + end + end + end + + H = adjoint(Ht) + return B, C, H, K, Ht +end + +""" + apply_Ht_to_b(Ht::AbstractMatrix, b::AbstractVector) + +Apply the accumulated left orthogonal transformation `H'` (stored as `Ht`) +to the constant vector `b`. + +# Arguments +- `Ht::AbstractMatrix`: The transpose of the orthogonal left factor `H`. +- `b::AbstractVector`: The vector to be transformed. + +# Returns +- `bhat::AbstractVector`: The transformed vector satisfying `bhat = Ht * b`. + +# Throws +- `DimensionMismatch`: If the number of columns of `Ht` does not match the length of `b`. +""" +function apply_Ht_to_b(Ht::AbstractMatrix, b::AbstractVector) + size(Ht, 2) == length(b) || throw(DimensionMismatch( + "Ht has $(size(Ht, 2)) columns but b has length $(length(b))" + )) + return Ht * b +end + +""" + bidiagonalize_with_H(A, L, b) + +Bidiagonalize `A` and apply the accumulated left transformation to `b`. + +# Arguments +- `A::AbstractMatrix`: The matrix to be bidiagonalized. +- `L::AbstractMatrix`: The matrix updated using the accumulated right transformations. +- `b::AbstractVector`: The vector to be transformed by the accumulated left transformation. + +# Returns +- `B::AbstractMatrix`: The bidiagonalized form of `A`. +- `C::AbstractMatrix`: The transformed form of `L`. +- `H::AbstractMatrix`: The left orthogonal factor. +- `K::AbstractMatrix`: The right orthogonal factor. +- `Ht::AbstractMatrix`: The transpose of `H`, representing the accumulated left transformations. +- `bhat::AbstractVector`: The transformed vector satisfying `bhat = Ht * b`. + +# Throws +- `DimensionMismatch`: If the number of columns of `Ht` does not equal the length of `b`. +""" +function bidiagonalize_with_H(A::AbstractMatrix, L::AbstractMatrix, b::AbstractVector,) + B, C, H, K, Ht = bidiagonalize_A(A, L) + bhat = Ht * b + + return B, C, H, K, Ht, bhat +end \ No newline at end of file diff --git a/test/Project.toml b/test/Project.toml index 0c36332..73141b0 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,2 +1,5 @@ [deps] +CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" \ No newline at end of file diff --git a/test/apply_Ht_to_b_tests.jl b/test/apply_Ht_to_b_tests.jl new file mode 100644 index 0000000..f65fc21 --- /dev/null +++ b/test/apply_Ht_to_b_tests.jl @@ -0,0 +1,42 @@ +module ApplyHtToBTests + +using Test +using LinearAlgebra +using ..RidgeRegression + +@testset "apply_Ht_to_b returns original vector when Ht is identity" begin + Ht = Matrix{Float64}(I, 3, 3) + b = [1.0, 2.0, 3.0] + + @test apply_Ht_to_b(Ht, b) == b +end + +@testset "apply_Ht_to_b correctly applies H^T to vector" begin + Ht =[1.0 0.0 0.0; + 0.0 0.0 1.0; + 0.0 1.0 0.0] + + b = [4.0, 5.0, 6.0] + + @test apply_Ht_to_b(Ht, b) == [4.0, 6.0, 5.0] +end + +@testset "apply_Ht_to_b correctly applies Givens rotation to vector" begin + c, s = 3/5, 4/5 + + Ht =[c s; + -s c] + + b = [5.0, 0.0] + + @test apply_Ht_to_b(Ht, b) ≈ [3.0, -4.0] +end + +@testset "pply_Ht_to_b throws DimensionMismatch" begin + Ht = Matrix{Float64}(I, 3, 3) + b = [1.0, 2.0] + + @test_throws DimensionMismatch apply_Ht_to_b(Ht, b) +end + +end \ No newline at end of file diff --git a/test/bidiagonalize_A_tests.jl b/test/bidiagonalize_A_tests.jl new file mode 100644 index 0000000..768e328 --- /dev/null +++ b/test/bidiagonalize_A_tests.jl @@ -0,0 +1,93 @@ +module BidiagonalizeATests + +using Test +using LinearAlgebra +using ..RidgeRegression + +@testset "bidiagonalize_A preserves orthogonality and factorization identities" begin + A = [1.0 2.0 3.0; + 4.0 5.0 6.0; + 7.0 8.0 10.0] + + L = Matrix{Float64}(I, 3, 3) + + B, C, H, K, Ht = bidiagonalize_A(A, L) + + @test Ht ≈ H' + + @test H' * H ≈ Matrix{Float64}(I, 3, 3) + @test H * H' ≈ Matrix{Float64}(I, 3, 3) + @test K' * K ≈ Matrix{Float64}(I, 3, 3) + @test K * K' ≈ Matrix{Float64}(I, 3, 3) + + @test H' * A * K ≈ B + @test L * K ≈ C +end + +@testset "bidiagonalize_A produces upper bidiagonal matrix" begin + A = [2.0 1.0 3.0; + 4.0 5.0 6.0; + 7.0 8.0 9.0] + + L = Matrix{Float64}(I, 3, 3) + + B, C, H, K, Ht = bidiagonalize_A(A, L) + + m, n = size(B) + + for i in 1:m + for j in 1:n + if !(j == i || j == i + 1) + @test isapprox(B[i, j], 0.0; atol=1e-10, rtol=0) + end + end + end +end + +@testset "bidiagonalize_A handles rectangular matrices" begin + A = [1.0 2.0 3.0; + 4.0 5.0 6.0; + 7.0 8.0 9.0; + 1.0 0.0 1.0] + + L = Matrix{Float64}(I, 3, 3) + + B, C, H, K, Ht = bidiagonalize_A(A, L) + + m, n = size(A) + + @test size(B) == (m, n) + @test size(C) == size(L) + @test size(H) == (m, m) + @test size(K) == (n, n) + @test size(Ht) == (m, m) + + @test H' * H ≈ Matrix{Float64}(I, m, m) + @test K' * K ≈ Matrix{Float64}(I, n, n) + + @test H' * A * K ≈ B + @test L * K ≈ C + + for i in 1:m + for j in 1:n + if !(j == i || j == i + 1) + @test isapprox(B[i, j], 0.0; atol=1e-10, rtol=0) + end + end + end +end + +@testset "bidiagonalize_A correctly zeroes subdiagonal entry in small matrix" begin + A = [3.0 0.0; + 4.0 5.0] + + L = Matrix{Float64}(I, 2, 2) + + B, C, H, K, Ht = bidiagonalize_A(A, L) + + @test H' * A * K ≈ B + @test L * K ≈ C + @test isapprox(B[2, 1], 0.0; atol=1e-12, rtol=0) +end + +end \ No newline at end of file diff --git a/test/bidiagonalize_with_H_tests.jl b/test/bidiagonalize_with_H_tests.jl new file mode 100644 index 0000000..5617894 --- /dev/null +++ b/test/bidiagonalize_with_H_tests.jl @@ -0,0 +1,28 @@ +module ApplyHtToBTests + +using Test +using LinearAlgebra +using ..RidgeRegression + +@testset "bidiagonalize_with_H applies Ht to b" begin + A = [1.0 2.0; + 3.0 4.0] + + L = Matrix{Float64}(I, 2, 2) + b = [5.0, 6.0] + + B, C, H, K, Ht, bhat = bidiagonalize_with_H(A, L, b) + + @test bhat ≈ Ht * b + @test bhat ≈ H' * b + + B2, C2, H2, K2, Ht2 = bidiagonalize_A(A, L) + + @test B ≈ B2 + @test C ≈ C2 + @test H ≈ H2 + @test K ≈ K2 + @test Ht ≈ Ht2 +end + +end \ No newline at end of file diff --git a/test/compute_givens_tests.jl b/test/compute_givens_tests.jl new file mode 100644 index 0000000..5a9d72f --- /dev/null +++ b/test/compute_givens_tests.jl @@ -0,0 +1,21 @@ +module computegivens_test + +using Test +using LinearAlgebra +using ..RidgeRegression + +@testset "Testset 1" begin + c, s = compute_givens(3.0, 0.0) + @test c == 1.0 + @test s == 0.0 + a = 3.0 + b = 4.0 + c, s = compute_givens(a, b) + v1 = c*a + s*b + v2 = -s*a + c*b + @test isapprox(v2, 0.0; atol=1e-12, rtol=0) + @test isapprox(abs(v1), hypot(a, b); atol=1e-12, rtol=0) + @test_throws ArgumentError compute_givens(0.0, 2.0) +end + +end \ No newline at end of file diff --git a/test/rotate_cols_tests.jl b/test/rotate_cols_tests.jl new file mode 100644 index 0000000..6dd21eb --- /dev/null +++ b/test/rotate_cols_tests.jl @@ -0,0 +1,17 @@ +module RotateColsTests + +using Test +using LinearAlgebra +using ..RidgeRegression + +@testset "rotate_cols! zeros second column entry using Givens rotation" begin + M = [3.0 4.0; + 1.0 2.0] + + c, s = compute_givens(3.0, 4.0) + rotate_cols!(M, 1, 2, c, s) + + @test isapprox(M[1, 2], 0.0; atol=1e-12, rtol=0) +end + +end \ No newline at end of file diff --git a/test/rotate_rows_tests.jl b/test/rotate_rows_tests.jl new file mode 100644 index 0000000..572ec42 --- /dev/null +++ b/test/rotate_rows_tests.jl @@ -0,0 +1,17 @@ +module RotateRowsTests + +using Test +using LinearAlgebra +using ..RidgeRegression + +@testset "rotate_rows! correctly applies Givens rotation to rows" begin + M = [1.0 2.0; + 3.0 4.0] + + c, s = compute_givens(1.0, 3.0) + rotate_rows!(M, 1, 2, c, s) + + @test isapprox(M[2, 1], 0.0; atol=1e-12, rtol=0) +end + +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index dbbe06f..95521cd 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,29 @@ using RidgeRegression using Test +using LinearAlgebra @testset "RidgeRegression.jl" begin - # Write your tests here. + @testset "Compute Givens Rotations Tests" begin + include("compute_givens_tests.jl") + end + + @testset "Rotate Columns Tests" begin + include("rotate_cols_tests.jl") + end + + @testset "Rotate Rows Tests" begin + include("rotate_rows_tests.jl") + end + + @testset "Bidiagonalization with A Tests" begin + include("bidiagonalize_A_tests.jl") + end + + @testset "Applying Ht to b Tests" begin + include("apply_Ht_to_b_tests.jl") + end + + @testset "Bidiagonalization with H Tests" begin + include("bidiagonalize_with_H_tests.jl") + end end