Skip to content

HypothesisTest & Checks

Chi-squared hypothesis test and integrity check modules used inside the hypothesis zkSNARK circuit. Sources: src/HypothesisTest.py, src/ComputeChecks.py.


Hypothesis Test

HypothesisTest

HypothesisTest(D, SD, MD, max_lut_val, lu_num_points, alpha=0.005, mode='ZKP')

Bases: Module

Chi-squared hypothesis test module for the zkSNARK compliance circuit.

Implements the statistical alarm decision over the DVAL-aggregated EKF residuals. Given the summed residual sum_dy and innovation covariance sum_CPCT_cov, computes the whitened chi-squared statistic and compares it against the upper control limit UCL = chi2.ppf(1 - alpha, df=MD). In ZKP mode, inputs carry one extra column of padding to maintain fixed ONNX tensor shapes across different DVAL configurations.

Parameters:

Name Type Description Default
D int

DVAL interval length (number of EKF steps per proof interval).

required
SD int

State space dimension.

required
MD int

Measurement space dimension (degrees of freedom).

required
max_lut_val float

Maximum value for the chi-squared CDF lookup table.

required
lu_num_points int

Number of points in the CDF lookup table.

required
alpha float

Significance level for the hypothesis test. Default 0.005.

0.005
mode str

'ZKP' for circuit-compatible mode, otherwise uses direct scipy CDF.

'ZKP'
Source code in src/HypothesisTest.py
def __init__(self,D,SD,MD,max_lut_val,lu_num_points,alpha=0.005,mode="ZKP"):

    super(HypothesisTest, self).__init__()
    self.forecast_horizon = D
    self.SD = SD
    self.MD = MD
    self.alpha = alpha
    self.mode=mode
    self.alpha_tensor = torch.tensor(alpha)
    UCL = chi2.ppf(1-alpha,df=self.MD)
    self.UCL_tensor = torch.tensor(UCL)
    self.SVD_check = SVDReconstructionCheck(self.MD)
    self.kalman_recon_check = KalmanReconstructionCheck()

forward

forward(sum_dy, sum_CPCT_cov, U, S, kcr_status_check)

Parameters:

Name Type Description Default
sum_dy Tensor

DVAL-summed residual vector. Shape (MD+1, 1) in ZKP mode.

required
sum_CPCT_cov Tensor

DVAL-summed innovation covariance. Shape (MD, MD+1) in ZKP mode.

required
U Tensor

Left singular vectors of sum_CPCT_cov for whitening. Shape (MD, MD+1) in ZKP mode.

required
S Tensor

Inverse square-root singular value matrix. Shape (MD, MD+1) in ZKP mode.

required
kcr_status_check Tensor

Accumulated Kalman check status across the DVAL interval.

required

Returns:

Type Description

Tuple of six tensors: - alarm_status: 1.0 if chi-squared statistic > UCL, else 0.0. - svd_check_status: 1.0 if SVD reconstruction check passes. - kcr_status: KCR check result (first element of kcr_status_check). - chi_squared_statistic: The computed test statistic (reported only; sensitive in production). - svd_diff: Element-wise SVD reconstruction error. - svd_threshold: Per-element tolerance used in the SVD check.

Source code in src/HypothesisTest.py
def forward(self, sum_dy, sum_CPCT_cov, U, S, kcr_status_check):
    """
    Args:
        sum_dy (torch.Tensor): DVAL-summed residual vector. Shape (MD+1, 1) in ZKP mode.
        sum_CPCT_cov (torch.Tensor): DVAL-summed innovation covariance. Shape (MD, MD+1) in ZKP mode.
        U (torch.Tensor): Left singular vectors of sum_CPCT_cov for whitening. Shape (MD, MD+1) in ZKP mode.
        S (torch.Tensor): Inverse square-root singular value matrix. Shape (MD, MD+1) in ZKP mode.
        kcr_status_check (torch.Tensor): Accumulated Kalman check status across the DVAL interval.

    Returns:
        Tuple of six tensors:
            - alarm_status: 1.0 if chi-squared statistic > UCL, else 0.0.
            - svd_check_status: 1.0 if SVD reconstruction check passes.
            - kcr_status: KCR check result (first element of kcr_status_check).
            - chi_squared_statistic: The computed test statistic (reported only; sensitive in production).
            - svd_diff: Element-wise SVD reconstruction error.
            - svd_threshold: Per-element tolerance used in the SVD check.
    """
    if self.mode == "ZKP":
        kcr_status_check = kcr_status_check.reshape(1,2)
        sum_dy = sum_dy.reshape(self.MD+1,1)
        sum_CPCT_cov = sum_CPCT_cov.reshape(self.MD, self.MD+1)

        sum_dy = sum_dy[:self.MD,:]
        sum_CPCT_cov = sum_CPCT_cov[:self.MD,:self.MD]

        U = U[:self.MD,:self.MD]
        S = S[:self.MD,:self.MD]
    else:
        kcr_status_check = kcr_status_check.reshape(1, 1)
        sum_dy = sum_dy.reshape(self.MD, 1)
        sum_CPCT_cov = sum_CPCT_cov.reshape(self.MD, self.MD)

        sum_dy = sum_dy[:self.MD, :]
        sum_CPCT_cov = sum_CPCT_cov[:self.MD, :self.MD]

        U = U[:self.MD, :self.MD]
        S = S[:self.MD, :self.MD]

    svd_check_status,svd_diff,svd_threshold = self.SVD_check.forward(sum_CPCT_cov,U,S)

    red_dy = U @ S @ sum_dy

    red_dy_T = torch.transpose(red_dy,0,1)

    chi_squared_statistic = red_dy_T @ red_dy

    alarm_status = torch.where(chi_squared_statistic > self.UCL_tensor, torch.tensor([1.0]), torch.tensor([0.0]))

    #chi_squared_statistic are being returned only for reporting purposes
    #in reality, chi_squared_statistic are sensitive values that must not be part of public proof output.
    return alarm_status, svd_check_status, kcr_status_check[:,:1], chi_squared_statistic, svd_diff, svd_threshold

CDF Lookup

Chi2CDFLookup

Chi2CDFLookup(num_points, MD)

Bases: Module

ONNX-compatible chi-squared CDF approximation via linear interpolation over a precomputed lookup table. Used inside the hypothesis test zkSNARK circuit.

Parameters:

Name Type Description Default
num_points int

Number of points in the lookup table.

required
MD int

Degrees of freedom for the chi-squared distribution.

required
Source code in src/HypothesisTest.py
def __init__(self,num_points,MD):
    self.num_points = num_points
    self.MD = MD
    super().__init__()

forward

forward(x, xs, cdf_vals)

Approximates chi2.cdf(x, df=MD) by linearly interpolating in the lookup table.

Parameters:

Name Type Description Default
x Tensor

The test statistic value to look up (scalar).

required
xs Tensor

Lookup table x-axis (num_points,).

required
cdf_vals Tensor

Precomputed CDF values at xs (num_points,).

required

Returns:

Type Description

torch.Tensor: Interpolated CDF value at x (scalar).

Source code in src/HypothesisTest.py
def forward(self, x, xs, cdf_vals):
    """
    Approximates chi2.cdf(x, df=MD) by linearly interpolating in the lookup table.

    Args:
        x (torch.Tensor): The test statistic value to look up (scalar).
        xs (torch.Tensor): Lookup table x-axis (num_points,).
        cdf_vals (torch.Tensor): Precomputed CDF values at xs (num_points,).

    Returns:
        torch.Tensor: Interpolated CDF value at x (scalar).
    """
    cdf_vals = cdf_vals.reshape(-1)
    xs = xs.reshape(-1)
    x = x.reshape(-1)

    # Ensure xs and cdf_vals have the same length
    assert len(xs) == len(cdf_vals), f"xs length ({len(xs)}) must match cdf_vals length ({len(cdf_vals)})"

    # Calculate the ratio of where x falls in the xs range
    ratio = (x[0] - xs[0]) / (xs[-1] - xs[0])

    # Calculate the continuous index
    idx = ratio * (len(cdf_vals) - 1)

    # Get integer indices for interpolation
    idx0_raw = torch.floor(idx).long()

    # Implement bounds checking without clamp
    # If idx0_raw < 0, use 0; if idx0_raw > max_idx, use max_idx
    max_idx = len(cdf_vals) - 2

    # Create condition masks
    too_small = (idx0_raw < 0).float()
    too_large = (idx0_raw > max_idx).float()
    in_range = 1.0 - too_small - too_large

    # Apply conditions
    idx0 = (too_small * 0 +
            too_large * max_idx +
            in_range * idx0_raw).long()

    idx1 = idx0 + 1

    # Calculate interpolation weight
    t = idx - idx0.float()

    # Use index_select for ONNX compatibility
    y0 = torch.index_select(cdf_vals, 0, idx0.reshape(-1)).squeeze()
    y1 = torch.index_select(cdf_vals, 0, idx1.reshape(-1)).squeeze()

    # Linear interpolation
    cdf = (1 - t) * y0 + t * y1

    return cdf

Chi2CDFLookupNonZKP

Chi2CDFLookupNonZKP(MD)

Bases: Module

Chi-squared CDF lookup using scipy directly. Not ONNX-compatible. Used only during non-ZKP testing and training to compute exact p-values.

Parameters:

Name Type Description Default
MD int

Degrees of freedom for the chi-squared distribution.

required
Source code in src/HypothesisTest.py
def __init__(self,MD):
    self.MD = MD
    super().__init__()

forward

forward(x, xs, cdf_vals)

Evaluates chi2.cdf(x, df=MD) via scipy. xs and cdf_vals are unused.

Source code in src/HypothesisTest.py
def forward(self, x, xs, cdf_vals):
    """Evaluates chi2.cdf(x, df=MD) via scipy. xs and cdf_vals are unused."""
    return torch.tensor(chi2.cdf(x.detach().numpy(),df=self.MD))

Integrity Checks

SVDReconstructionCheck

SVDReconstructionCheck(MD, rtol=1e-12, atol=0.1)

Bases: Module

Verifies that an SVD decomposition of the residual covariance is valid.

Given U and S (where S holds the inverse square-root singular values), checks whether U @ S @ S @ U^T ≈ I. This confirms that the SVD factors passed into the hypothesis circuit correctly represent the inverse of sum_CPCT_cov, which is required for the chi-squared whitening step.

Parameters:

Name Type Description Default
MD int

Dimension of the measurement space (number of sensors).

required
rtol float

Relative tolerance for the allclose check. Default 1e-12.

1e-12
atol float

Absolute tolerance for the allclose check. Default 0.1.

0.1
Source code in src/ComputeChecks.py
def __init__(self,MD,rtol=1e-12, atol=0.1):
    super().__init__()
    self.rtol = rtol

    self.atol = float(os.environ.get("MAX_SVD_TOL",atol))
    #self.atol = atol
    self.MD = MD
    self.Ir = torch.eye(self.MD,self.MD)

forward

forward(sum_CPCT_cov, U, S)

Parameters:

Name Type Description Default
sum_CPCT_cov Tensor

DVAL-aggregated residual covariance (MD x MD).

required
U Tensor

Left singular vectors from SVD of sum_CPCT_cov (MD x MD).

required
S Tensor

Diagonal matrix of inverse square-root singular values (MD x MD).

required

Returns:

Type Description

Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: - is_allclose (torch.Tensor): Scalar in {0, 1}; 1 if reconstruction passes. - diff (torch.Tensor): Element-wise absolute difference |I - U S S U^T|. - threshold (torch.Tensor): Per-element tolerance used for the check.

Source code in src/ComputeChecks.py
def forward(self, sum_CPCT_cov, U, S):
    """
    Args:
        sum_CPCT_cov (torch.Tensor): DVAL-aggregated residual covariance (MD x MD).
        U (torch.Tensor): Left singular vectors from SVD of sum_CPCT_cov (MD x MD).
        S (torch.Tensor): Diagonal matrix of inverse square-root singular values (MD x MD).

    Returns:
        Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
            - is_allclose (torch.Tensor): Scalar in {0, 1}; 1 if reconstruction passes.
            - diff (torch.Tensor): Element-wise absolute difference |I - U S S U^T|.
            - threshold (torch.Tensor): Per-element tolerance used for the check.
    """
    sum_CPCT_cov = sum_CPCT_cov.view(self.MD, self.MD)
    U = U.view(self.MD, self.MD)
    S = S.view(self.MD, self.MD)
    U_T = torch.transpose(U,0,1)
    sum_CPCT_cov_inv = U @ S @ S @ U_T
    rhs = sum_CPCT_cov_inv @ sum_CPCT_cov

    # Simulate allclose: check if max(abs(A - B)) < atol + rtol * abs(B)
    diff = torch.abs(self.Ir - rhs)
    threshold = self.atol + self.rtol * torch.abs(rhs)
    comp = diff < threshold

    is_allclose_tensor = comp.flatten().float().min()

    return is_allclose_tensor.unsqueeze(0), diff, threshold

KalmanReconstructionCheck

KalmanReconstructionCheck(rtol=1e-12, atol=0.1)

Bases: Module

Verifies the optimality of the Kalman gain supplied to the prediction circuit.

Checks whether CPCT_cov^T @ K^T ≈ C @ P^T, which is the condition implied by the optimal Kalman gain K = P C^T (C P C^T + R)^{-1}. This prevents a utility from supplying an incorrect gain that would distort the residual.

Parameters:

Name Type Description Default
rtol float

Relative tolerance. Default 1e-12.

1e-12
atol float

Absolute tolerance. Default 0.1.

0.1
Source code in src/ComputeChecks.py
def __init__(self,rtol=1e-12, atol=0.1):
    super().__init__()
    self.rtol = rtol
    self.atol = float(os.environ.get("MAX_KCR_TOL",atol))

forward

forward(CPCT_cov, jac_Ch, P, KT)

Parameters:

Name Type Description Default
CPCT_cov Tensor

Innovation covariance C P C^T + R (batch x MD x MD).

required
jac_Ch Tensor

Jacobian of the observation function C w.r.t. h (batch x MD x SD).

required
P Tensor

State error covariance (batch x SD x SD).

required
KT Tensor

Transposed Kalman gain K^T (batch x MD x SD).

required

Returns:

Type Description

Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: - is_allclose (torch.Tensor): Scalar in {0, 1}; 1 if the gain check passes. - diff (torch.Tensor): Element-wise absolute difference |CPCT_cov^T K^T - C P^T|. - threshold (torch.Tensor): Per-element tolerance used for the check.

Source code in src/ComputeChecks.py
def forward(self, CPCT_cov, jac_Ch, P, KT):
    """
    Args:
        CPCT_cov (torch.Tensor): Innovation covariance C P C^T + R (batch x MD x MD).
        jac_Ch (torch.Tensor): Jacobian of the observation function C w.r.t. h (batch x MD x SD).
        P (torch.Tensor): State error covariance (batch x SD x SD).
        KT (torch.Tensor): Transposed Kalman gain K^T (batch x MD x SD).

    Returns:
        Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
            - is_allclose (torch.Tensor): Scalar in {0, 1}; 1 if the gain check passes.
            - diff (torch.Tensor): Element-wise absolute difference |CPCT_cov^T K^T - C P^T|.
            - threshold (torch.Tensor): Per-element tolerance used for the check.
    """
    PT = P.transpose(1,2)
    CPT = torch.bmm(jac_Ch, PT)
    CPCT_cov_T = CPCT_cov.transpose(1, 2)
    CPCT_covT_KT = CPCT_cov_T @ KT
    diff = torch.abs(CPCT_covT_KT - CPT)

    threshold = self.atol + self.rtol * torch.abs(CPT)
    comp = diff < threshold

    is_allclose_tensor = comp.flatten().float().min()

    return is_allclose_tensor.unsqueeze(0),diff, threshold