Add Satterthwaite and Kenward-Roger fixed-effect tests

This commit is contained in:
2026-08-13 07:02:10 +08:00
parent 76dd8b1379
commit 8557daecac
14 changed files with 1330 additions and 20 deletions

View File

@@ -0,0 +1,13 @@
#pragma once
namespace spectra::reml {
// Two-sided Student-t tail probability and upper F tail probability.
// Both accept non-integer positive degrees of freedom.
[[nodiscard]] double student_t_two_sided_p(double statistic,
double degrees_of_freedom);
[[nodiscard]] double f_upper_tail(double statistic,
double numerator_degrees_of_freedom,
double denominator_degrees_of_freedom);
} // namespace spectra::reml

View File

@@ -0,0 +1,27 @@
#pragma once
#include "spectra_reml/types.hpp"
#include <vector>
namespace spectra::reml {
struct FixedEffectHypothesis {
// Rows are restrictions and columns correspond to beta in design order.
ColMajorMatrix contrast;
// Empty means a zero right-hand side.
std::vector<double> rhs;
};
// Computes coefficient-wise tests and any supplied general linear hypotheses
// at an already fitted REML solution. Inputs must use the same (possibly GRM-
// rotated) coordinate system and phenotype scale as the supplied fit.
[[nodiscard]] FixedEffectInferenceResult infer_fixed_effects_spectral(
const std::vector<double>& y_star, const ColMajorMatrix& x_star,
const std::vector<double>& eigenvalues, const RemlResult& fit,
FixedEffectTestMethod method,
const std::vector<FixedEffectHypothesis>& hypotheses = {},
double rank_tolerance_relative = 1e-10,
double covariance_floor_relative = 1e-12);
} // namespace spectra::reml

View File

@@ -64,6 +64,49 @@ enum class FitStatus {
[[nodiscard]] const char* to_string(FitStatus status) noexcept;
enum class FixedEffectTestMethod {
none,
satterthwaite,
kenward_roger
};
[[nodiscard]] const char* to_string(FixedEffectTestMethod method) noexcept;
enum class FixedEffectInferenceStatus {
not_requested,
ok,
boundary_conditional,
fit_not_converged,
invalid_contrast,
information_singular,
numerical_error
};
[[nodiscard]] const char* to_string(FixedEffectInferenceStatus status) noexcept;
struct FixedEffectTestResult {
bool valid = false;
std::size_t numerator_df = 0;
double denominator_df = std::numeric_limits<double>::quiet_NaN();
double statistic = std::numeric_limits<double>::quiet_NaN();
double p_value = std::numeric_limits<double>::quiet_NaN();
double estimate = std::numeric_limits<double>::quiet_NaN();
double standard_error = std::numeric_limits<double>::quiet_NaN();
std::string error;
};
struct FixedEffectInferenceResult {
FixedEffectTestMethod method = FixedEffectTestMethod::none;
FixedEffectInferenceStatus status =
FixedEffectInferenceStatus::not_requested;
// One test per beta, in design-matrix column order. Satterthwaite reports
// a signed t statistic; Kenward-Roger reports an F statistic with 1 NumDF.
std::vector<FixedEffectTestResult> coefficient_tests;
// Optional general linear hypotheses requested by the caller.
std::vector<FixedEffectTestResult> hypothesis_tests;
std::string error;
};
struct RemlOptions {
std::size_t max_iterations = 100;
std::size_t line_search_max_evaluations = 48;
@@ -95,6 +138,12 @@ struct RemlOptions {
// sqrt(OLS residual mean square / 2).
double initial_sigma_e = std::numeric_limits<double>::quiet_NaN();
double initial_sigma_g = std::numeric_limits<double>::quiet_NaN();
// Batch fixed-effect inference. The numerical fit API itself remains
// usable without inference; run_task_batch invokes the inference engine at
// the final REML estimate and defaults to Satterthwaite t/F tests.
FixedEffectTestMethod fixed_effect_test =
FixedEffectTestMethod::satterthwaite;
};
struct RemlResult {
@@ -113,6 +162,8 @@ struct RemlResult {
// Row-wise packed lower triangle:
// (0,0), (1,0), (1,1), (2,0), (2,1), (2,2), ...
std::vector<double> beta_covariance_packed_lower;
FixedEffectInferenceResult fixed_effect_inference;
FixedEffectTestResult extra_fixed_effect_joint_test;
std::string error;
[[nodiscard]] bool has_estimates() const noexcept {