---
title: "Manual test"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Manual test}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>", eval = FALSE)
```

## Purpose

This validation reconstructs the fitted parameters of `gipslda()`,
`gipsqda()`, and `gipsmultqda()` independently. The reference calculation uses
only base R, `stats`, and the public `gips` API. It does not call any `gipsDA`
helper or internal function.

The executable version is
`tests/testthat/manual/test-manual-parameter-reconstruction.R`. The formulas
and implementation notes are kept beside the calculations in that test so
that differences between the reference and package implementations remain
reviewable.

## Mock data in \(\mathbb{R}^3\)

Three Gaussian classes are generated with sample sizes 9, 10, and 11. Their
means are

\[
\mu_A=(-1.5,0.2,0.8),\quad
\mu_B=(0.6,1.7,-0.5),\quad
\mu_C=(1.4,-1.0,1.2).
\]

Each class has a different positive-definite covariance matrix:

\[
\Sigma_A =
\begin{pmatrix}
1.20&0.35&0.10\\
0.35&0.80&-0.15\\
0.10&-0.15&0.65
\end{pmatrix},\quad
\Sigma_B =
\begin{pmatrix}
0.75&-0.20&0.18\\
-0.20&1.35&0.30\\
0.18&0.30&0.90
\end{pmatrix},
\]

\[
\Sigma_C =
\begin{pmatrix}
1.05&0.25&-0.22\\
0.25&0.70&0.12\\
-0.22&0.12&1.25
\end{pmatrix}.
\]

With a fixed seed, standard-normal matrices \(Z_g\) are transformed as

\[
X_g = Z_g\,\operatorname{chol}(\Sigma_g) + \mu_g.
\]

The unequal class sizes test the empirical priors and the sample-size vector
used by the joint projection. Three dimensions keep brute-force permutation
search small and deterministic.

## Covariance projection

For every reference projection, the test constructs a `gips` object directly:

```r
search <- gips::gips(
  empirical_covariances,
  sample_sizes,
  was_mean_estimated = TRUE
)
search <- gips::find_MAP(
  search,
  optimizer = "BF",
  show_progress_bar = FALSE
)
permutation <- search[[1L]]
projected <- lapply(
  empirical_covariances,
  gips::project_matrix,
  permutation
)
```

When only one covariance matrix is projected, that matrix is passed directly
to `gips::gips()`. The brute-force optimizer enumerates the permutations in
\(\mathbb{R}^3\), avoiding Monte Carlo variation.

The projected covariance \(S\) is regularized only if its eigenvalue
\(\lambda\) nearest zero satisfies \(|\lambda| < 0.05\). In that case,

\[
s=\frac{0.05-\lambda}{1-0.05},
\qquad
S^*=\frac{S+sI}{1+s}.
\]

This transformation is implemented explicitly in the test.

## LDA reconstruction

Let \(M_g\) denote the sample mean of class \(g\), and let
\(E_i=x_i-M_{g_i}\) be the within-class residuals. Define

\[
D=\operatorname{diag}\left(
\frac{1}{\sqrt{\operatorname{diag}(\operatorname{var}(E))}}
\right).
\]

For \(n\) observations and \(G\) classes, the covariance supplied to `gips`
is calculated manually as

\[
S_W=\frac{n-1}{n-G}\operatorname{cov}(ED).
\]

Here `stats::cov()` uses the usual denominator `n - 1`, and the additional
factor `(n - 1) / (n - G)` converts the standardized residual covariance to the
pooled within-class covariance scale used by the implementation.

After MAP projection and regularization, write
\(S_W^*=V\operatorname{diag}(d)V^\mathsf{T}\). The whitening transform is

\[
W=DV\operatorname{diag}(d^{-1/2}).
\]

Using the empirical priors \(\pi_g=n_g/n\) and
\(\bar{x}=\sum_g\pi_gM_g\), the between-class matrix is

\[
B_g=
\sqrt{\frac{n\pi_g}{G-1}}\,(M_g-\bar{x})W.
\]

If \(B=U_B\operatorname{diag}(d_B)V_B^\mathsf{T}\), the stored discriminant
scaling is \(WV_B\), truncated using the same numerical rank rule as the
public model. The test independently compares:

- empirical priors, class counts, means, labels, and sample size;
- the MAP permutation;
- between-class singular values;
- the discriminant scaling after resolving arbitrary SVD column signs.

## Separate QDA reconstruction

For each class, the sample covariance

\[
S_g=\operatorname{cov}(X_g)
\]

is projected in a separate brute-force `gips` search. For each class, the class sample size \(n_g\) is supplied to the corresponding `gips` search, matching the class-specific covariance estimator used by `gipsqda()`. If

\[
S_g^*=V_g\operatorname{diag}(d_g)V_g^\mathsf{T},
\]

the reference parameters are

\[
L_g=V_g\operatorname{diag}(d_g^{-1/2}),
\qquad
\log|S_g^*|=\sum_j\log d_{gj}.
\]

Because singular vectors can change sign without changing the model, the test
compares \(L_gL_g^\mathsf{T}\), the precision matrix used by QDA, rather than
the signs of individual columns.

## Joint QDA reconstruction

The joint model starts from the same class covariances but passes
\((S_A,S_B,S_C)\) and the sample-size vector \((9,10,11)\) to one `gips`
search. Its single MAP permutation is then used to project every covariance.
The scaling matrices and log determinants are calculated from the resulting
matrices using the QDA equations above.

This distinguishes `gipsmultqda()` from `gipsqda()`: the former estimates one
shared symmetry, while the latter estimates each class symmetry independently.

## Assertions

The three public fitting functions are called only after all reference
parameters have been computed. Numerical quantities are compared with
tolerance \(10^{-10}\). Discrete quantities, dimensions, labels, sample size,
and MAP permutations are compared exactly. The QDA scaling checks use
precision matrices, and the LDA scaling check aligns SVD signs before applying
the tolerance.

The test therefore validates the complete set of fitted numerical parameters,
apart from the recorded call, against a calculation that is independent of
the package implementation.
