# -*- tcl -*-

source [file join \
        [file dirname [file dirname [file join [pwd] [info script]]]] \
        devtools testutilities.tcl]

testsNeedTcl     8.6
testsNeedTcltest 1.0

support {
    useLocal math.tcl       math
    useLocal linalg.tcl     math::linearalgebra
    useLocal statistics.tcl math::statistics
}
testing {
    useLocal pca.tcl math::PCA
}

#package require math::statistics

#
# Matching procedure - flatten the lists
#
proc matchNumbers {expected actual} {
    if {$actual=="" && $expected!=""} then {
        return 0
    }
    if {$actual!="" && $expected==""} then {
        return 0
    }
    set match 1
    if { [llength [lindex $expected 0]] > 1 } {
        foreach a $actual e $expected {
            set match [matchNumbers $e $a]
            if { $match == 0 } {
                break
            }
        }
    } else {

        foreach a $actual e $expected {
            if {[string is double $a]==0  || [string is double $e]==0} then {
                return 0
            }
            if {abs($a-$e) > 0.1e-6} {
                set match 0
                break
            }
        }
    }
    return $match
}

customMatch numbers matchNumbers

# Test the normalise/denormalise procedures

test normalise-1.0 "Normalise a vector" -match numbers -body {
    ::math::PCA::Normalise {1.0 2.0 3.0} {0.0 1.0 2.0} {2.0 2.0 2.0}
} -result {0.5 0.5 0.5}

test normalise-1.1 "Denormalise a vector" -match numbers -body {
    ::math::PCA::Denormalise {0.5 0.5 0.5} {0.0 1.0 2.0} {2.0 2.0 2.0}
} -result {1.0 2.0 3.0}

set plusminus   {{ 3.0 -3.0 -3.0  3.0}
                 {-3.0  3.0  3.0 -3.0}
                 { 0.0  0.0  0.0  0.0}
                 {-3.0  3.0 -3.0  3.0}
                 { 3.0 -3.0  3.0 -3.0}}

set transformed {{ 1.0 -1.0 -1.0  1.0}
                 {-1.0  1.0  1.0 -1.0}
                 { 0.0  0.0  0.0  0.0}
                 {-1.0  1.0 -1.0  1.0}
                 { 1.0 -1.0  1.0 -1.0}}

set zeroes      {0.0 0.0 0.0 0.0}
set threes      {3.0 3.0 3.0 3.0}

set data        {{7 4 3}
                 {4 1 8}
                 {6 3 5}
                 {8 6 1}
                 {8 5 7}
                 {7 2 9}
                 {5 3 3}
                 {9 5 8}
                 {7 4 5}
                 {8 2 2}}
set ones        {1.0 1.0 1.0}

test normalise-1.2 "Check transformation - transformed matrix" -match numbers -body {
    lindex [::math::PCA::Transform $plusminus 1] 0
} -result $transformed

test normalise-1.3 "Check transformation - averages" -match numbers -body {
    lindex [::math::PCA::Transform $plusminus 1] 1
} -result $zeroes

test normalise-1.4 "Check transformation - scale values" -match numbers -body {
    lindex [::math::PCA::Transform $plusminus 1] 2
} -result $threes

test normalise-1.5 "Check transformation - unit standard deviations" -match numbers -body {
    set transformedData  [lindex [::math::PCA::Transform $data 1] 0]
    set transposedMatrix [::math::linearalgebra::transpose $transformedData]

    set stdevs {}
    foreach vector $transposedMatrix {
        lappend stdevs [::math::statistics::stdev $vector]
    }
    set stdevs
} -result $ones

test normalise-1.6 "Check transformation - retain lengths" -match numbers -body {
    lindex [::math::PCA::Transform $data 0] 2
} -result $ones

test create-1.1 "Create a PCA object correctly" -match glob -setup {
    set pca {}
} -body {
    set pca [::math::PCA::createPCA $data]
} -cleanup {
    if { $pca ne "" } {
        $pca destroy
    }
} -returnCodes 0 -result *

test create-1.2 "Create a PCA object correctly - 2" -match glob -setup {
    set pca {}
} -body {
    set pca [::math::PCA::createPCA $data -covariance 0]
} -cleanup {
    if { $pca ne "" } {
        $pca destroy
    }
} -returnCodes 0 -result *

test create-1.3 "Create a PCA object incorrectly" -match glob -setup {
    set pca {}
} -body {
    ::math::PCA::createPCA $zeroes
} -cleanup {
    if { $pca ne "" } {
        $pca destroy
    }
} -returnCodes 1 -result *

test eigenvectors-1.1 "Get the eigenvectors - all" -match numbers -setup {
    set pca [::math::PCA::createPCA $data]
} -body {
    set eigenvectors [$pca eigenvectors]
} -cleanup {
    if { $pca ne "" } {
        $pca destroy
    }
} -result {
    {0.6420045763498947 -0.38467229168844363 -0.6632174243435957}
    {0.6863616413605773 -0.0971303301343052 0.7207450285897333}
    {-0.3416691692479824 -0.9179286066874492 0.20166618906061484}}

test eigenvectors-1.2 "Get the eigenvalues - all" -match numbers -setup {
    set pca [::math::PCA::createPCA $data]
} -body {
    set eigenvalues [$pca eigenvalues]
} -cleanup {
    if { $pca ne "" } {
        $pca destroy
    }
} -result {1.768774136103425 0.9270759168988945 0.30414994699768233}

test eigenvectors-1.3 "Restrict to two components - eigenvectors" -match numbers -setup {
    set pca [::math::PCA::createPCA $data]
} -body {
    $pca using 2
    set eigenvectors [$pca eigenvectors]
} -cleanup {
    if { $pca ne "" } {
        $pca destroy
    }
} -result {
    {0.6420045763498947 -0.38467229168844363}
    {0.6863616413605773 -0.0971303301343052}
    {-0.3416691692479824 -0.9179286066874492}}

test eigenvectors-1.4 "Restrict to two components - eigenvalues" -match numbers -setup {
    set pca [::math::PCA::createPCA $data]
} -body {
    $pca using 2
    set eigenvalues [$pca eigenvalues]
} -cleanup {
    if { $pca ne "" } {
        $pca destroy
    }
} -result {1.768774136103425 0.9270759168988945}

test eigenvectors-1.5 "Restrict to two components - get all eigenvectors" -match numbers -setup {
    set pca [::math::PCA::createPCA $data]
} -body {
    global pca
    $pca using 2
    set eigenvectors [$pca eigenvectors -all]
} -cleanup {
    if { $pca ne "" } {
        $pca destroy
    }
} -result {
    {0.6420045763498947 -0.38467229168844363 -0.6632174243435957}
    {0.6863616413605773 -0.0971303301343052 0.7207450285897333}
    {-0.3416691692479824 -0.9179286066874492 0.20166618906061484}}

test eigenvectors-1.6 "Restrict to two components - get all eigenvalues" -match numbers -setup {
    set pca [::math::PCA::createPCA $data]
} -body {
    $pca using 2
    set eigenvalues [$pca eigenvalues -all]
} -cleanup {
    if { $pca ne "" } {
        $pca destroy
    }
} -result {1.768774136103425 0.9270759168988945 0.30414994699768233}

test eigenvectors-1.7 "Use all components again" -match numbers -setup {
    set pca [::math::PCA::createPCA $data]
} -body {
    $pca using 3
    set eigenvectors [$pca eigenvectors]
} -cleanup {
    if { $pca ne "" } {
        $pca destroy
    }
} -result {
    {0.6420045763498947 -0.38467229168844363 -0.6632174243435957}
    {0.6863616413605773 -0.0971303301343052 0.7207450285897333}
    {-0.3416691692479824 -0.9179286066874492 0.20166618906061484}}

test approximation-1.1 "Approximate an observation" -match numbers -setup {
    set pca [::math::PCA::createPCA $data]
} -body {
    $pca using 2
    set approximation [$pca approximate [lindex $::data 0]]
} -cleanup {
    if { $pca ne "" } {
        $pca destroy
    }
} -result {7.03386897006181 3.961810336299144 2.981031668611465}

test approximation-1.2 "Approximate an observation, get the scores" -match numbers -setup {
    set pca [::math::PCA::createPCA $data]
} -body {
    $pca using 2
    set scores [$pca scores [lindex $::data 0]]
} -result {0.5148128141212557 0.630835559461938}

test approximation-1.3 "Approximate an observation, get the Q statistic" -match numbers -setup {
    set pca [::math::PCA::createPCA $data]
} -body {
    $pca using 2
    set qstatistic [$pca qstatistic [lindex $::data 0]]
} -cleanup {
    if { $pca ne "" } {
        $pca destroy
    }
} -result 0.001123022217614874

test approximation-1.4 "Approximate an observation, get the corrected Q statistic" -match numbers -setup {
    set pca [::math::PCA::createPCA $data]
} -body {
    $pca using 2
    set qstatistic [$pca qstatistic [lindex $::data 0] -original]
} -cleanup {
    if { $pca ne "" } {
        $pca destroy
    }
} -result 0.0016043174537355342

test approximation-2.1 "Approximate all original data (actually reconstruct)" -match numbers -setup {
    set pca [::math::PCA::createPCA $data]
} -body {
    # Note: as we use all components, we reconstruct the original data "exactly"
    $pca using 3
    set approximation [$pca approximateOriginal]
} -cleanup {
    if { $pca ne "" } {
        $pca destroy
    }
} -result $data
