def predict_smo(X_train, y_train, alpha, b, X_test, kernel, **kwargs):
    if kernel is rbf_kernel:
        K = kernel(X_test, X_train, **kwargs)
    else:
        K = kernel(X_test, X_train)
    scores = K @ (alpha * y_train) + b
    return np.sign(scores)


if __name__ == "__main__":
    rng = np.random.default_rng(0)
    n = 100
    X = rng.normal(size=(n, 2))
    y = np.where(X[:, 0] ** 2 + X[:, 1] ** 2 < 1.0, 1, -1).astype(float)

    split = int(0.8 * n)
    X_train, X_test = X[:split], X[split:]
    y_train, y_test = y[:split], y[split:]

    alpha, b = simplified_smo(
        X_train, y_train, C=1.0,
        kernel=lambda a, c: rbf_kernel(a, c, gamma=1.0),
    )
    preds = predict_smo(
        X_train, y_train, alpha, b, X_test,
        kernel=rbf_kernel, gamma=1.0,
    )
    acc = np.mean(preds == y_test)
    n_support = np.sum(alpha > 1e-5)
    print(f"RBF-kernel SVM test accuracy: {acc:.3f}")
    print(f"Support vectors used: {n_support} / {len(y_train)}")
