% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/lgspline.R
\name{lgspline}
\alias{lgspline}
\title{Fit Lagrangian Multiplier Smoothing Splines}
\usage{
lgspline(
  predictors = NULL,
  y = NULL,
  formula = NULL,
  response = NULL,
  standardize_response = TRUE,
  standardize_predictors_for_knots = TRUE,
  standardize_expansions_for_fitting = TRUE,
  family = gaussian(),
  glm_weight_function = function(mu, y, order_indices, family, dispersion,
    observation_weights, ...) {
     if (any(!is.null(observation_weights))) {
        
    family$variance(mu) * observation_weights
     }
     else {
        
    family$variance(mu)
     }
 },
  schur_correction_function = function(X, y, B, dispersion, order_list, K, family,
    observation_weights, ...) {
     lapply(1:(K + 1), function(k) 0)
 },
  need_dispersion_for_estimation = FALSE,
  dispersion_function = function(mu, y, order_indices, family, observation_weights,
    VhalfInv, ...) {
     if (!is.null(VhalfInv)) {
         VhalfInv <-
    VhalfInv[order_indices, order_indices]
         c(mean((tcrossprod(VhalfInv, t(y -
    mu)))^2/family$variance(mu)))
     }
     else {
         c(mean((y -
    mu)^2/family$variance(mu)))
     }
 },
  K = NULL,
  custom_knots = NULL,
  cluster_on_indicators = FALSE,
  make_partition_list = NULL,
  previously_tuned_penalties = NULL,
  smoothing_spline_penalty = NULL,
  opt = TRUE,
  use_custom_bfgs = TRUE,
  delta = NULL,
  tol = 10 * sqrt(.Machine$double.eps),
  tuning_criterion = "loo",
  gcv_gamma = 1.4,
  initial_wiggle = c(2e-12, 2e-07, 2e-04, 0.2),
  initial_flat = c(0.5, 5),
  wiggle_penalty = 2e-07,
  flat_ridge_penalty = 0.5,
  unique_penalty_per_partition = TRUE,
  unique_penalty_per_predictor = TRUE,
  meta_penalty = 1e-08,
  predictor_penalties = NULL,
  partition_penalties = NULL,
  include_quadratic_terms = TRUE,
  include_cubic_terms = TRUE,
  include_quartic_terms = NULL,
  include_2way_interactions = TRUE,
  include_3way_interactions = TRUE,
  include_quadratic_interactions = FALSE,
  offset = c(),
  just_linear_with_interactions = NULL,
  just_linear_without_interactions = NULL,
  exclude_interactions_for = NULL,
  exclude_these_expansions = NULL,
  custom_basis_fxn = NULL,
  include_constrain_fitted = TRUE,
  include_constrain_first_deriv = TRUE,
  include_constrain_second_deriv = TRUE,
  include_constrain_interactions = TRUE,
  cl = NULL,
  chunk_size = NULL,
  parallel_eigen = TRUE,
  parallel_trace = FALSE,
  parallel_aga = FALSE,
  parallel_matmult = FALSE,
  parallel_unconstrained = TRUE,
  parallel_find_neighbors = FALSE,
  parallel_penalty = FALSE,
  parallel_make_constraint = FALSE,
  unconstrained_fit_fxn = unconstrained_fit_default,
  keep_weighted_Lambda = FALSE,
  iterate_tune = TRUE,
  iterate_final_fit = TRUE,
  blockfit = TRUE,
  qp_score_function = function(X, y, mu, order_list, dispersion, VhalfInv,
    observation_weights, ...) {
     if (!is.null(observation_weights)) {
        
    crossprod(X, cbind((y - mu) * observation_weights))
     }
     else {
        
    crossprod(X, cbind(y - mu))
     }
 },
  qp_observations = NULL,
  qp_Amat = NULL,
  qp_bvec = NULL,
  qp_meq = 0,
  qp_positive_derivative = FALSE,
  qp_negative_derivative = FALSE,
  qp_positive_2ndderivative = FALSE,
  qp_negative_2ndderivative = FALSE,
  qp_monotonic_increase = FALSE,
  qp_monotonic_decrease = FALSE,
  qp_range_upper = NULL,
  qp_range_lower = NULL,
  qp_Amat_fxn = NULL,
  qp_bvec_fxn = NULL,
  qp_meq_fxn = NULL,
  constraint_values = cbind(),
  constraint_vectors = cbind(),
  return_G = TRUE,
  return_Ghalf = TRUE,
  return_U = TRUE,
  estimate_dispersion = TRUE,
  unbias_dispersion = NULL,
  return_varcovmat = TRUE,
  exact_varcovmat = FALSE,
  return_lagrange_multipliers = FALSE,
  custom_penalty_mat = NULL,
  cluster_args = c(custom_centers = NA, nstart = 10),
  dummy_dividor = 1.2345672152894e-22,
  dummy_adder = 2.234567210529e-18,
  verbose = FALSE,
  verbose_tune = FALSE,
  dummy_fit = FALSE,
  auto_encode_factors = TRUE,
  observation_weights = NULL,
  do_not_cluster_on_these = c(),
  neighbor_tolerance = 1 + 1e-08,
  null_constraint = NULL,
  critical_value = qnorm(1 - 0.05/2),
  data = NULL,
  weights = NULL,
  no_intercept = FALSE,
  correlation_id = NULL,
  spacetime = NULL,
  correlation_structure = NULL,
  VhalfInv = NULL,
  Vhalf = NULL,
  VhalfInv_fxn = NULL,
  Vhalf_fxn = NULL,
  VhalfInv_par_init = c(),
  REML_grad = NULL,
  custom_VhalfInv_loss = NULL,
  VhalfInv_logdet = NULL,
  include_warnings = TRUE,
  penalty_args = NULL,
  tuning_args = NULL,
  expansion_args = NULL,
  constraint_args = NULL,
  qp_args = NULL,
  parallel_args = NULL,
  covariance_args = NULL,
  return_args = NULL,
  glm_args = NULL,
  ...
)
}
\arguments{
\item{predictors}{Default: NULL. Numeric matrix or data frame of predictor
variables, or a formula when using the formula interface.}

\item{y}{Default: NULL. Numeric response variable vector.}

\item{formula}{Default: NULL. Optional statistical formula for model specification,
supporting \code{spl()} (and the alias \code{s()}) for spline terms.}

\item{response}{Default: NULL. Alternative name for response variable.}

\item{standardize_response}{Default: TRUE. Logical indicator controlling whether
the response variable should be centered and scaled before model fitting. Only
offered for identity link functions.}

\item{standardize_predictors_for_knots}{Default: TRUE. Logical flag controlling
whether predictors are internally standardized for partitioning / knot
placement. The exact transformation is handled inside
\code{\link{make_partitions}} and depends on the effective clustering
dimension.}

\item{standardize_expansions_for_fitting}{Default: TRUE. Logical switch to
standardize polynomial basis expansions during model fitting. Design matrices,
variance-covariance matrices, and coefficients are backtransformed after fitting.
\eqn{\mathbf{U}} and \eqn{\mathbf{G}} remain on the transformed scale;
\code{B_raw} corresponds to coefficients on the expansion-standardized scale.}

\item{family}{Default: \code{gaussian()}. GLM family specifying the error distribution
and link function. Minimally requires: family name, link name, linkfun, linkinv,
variance.}

\item{glm_weight_function}{Default: function returning \code{family$variance(mu)},
optionally multiplied by \code{observation_weights}. Codes the mean-variance
relationship (the diagonal \eqn{\mathbf{W}} matrix) used for updating
\eqn{\mathbf{G}} after obtaining constrained estimates.}

\item{schur_correction_function}{Default: function returning list of zeros.
Computes Schur complements \eqn{\mathbf{S}} added to \eqn{\mathbf{G}}:
\eqn{\mathbf{G}^{*} = (\mathbf{G}^{-1} + \mathbf{S})^{-1}}.}

\item{need_dispersion_for_estimation}{Default: FALSE. Logical indicator specifying
whether a dispersion parameter is required for coefficient estimation (e.g.
Weibull AFT).}

\item{dispersion_function}{Default: function returning mean squared residuals.
Custom function for estimating the exponential dispersion parameter.}

\item{K}{Default: NULL. Integer specifying the number of knot locations. Intuitively,
total partitions minus 1.}

\item{custom_knots}{Default: NULL. Optional matrix providing user-specified knot
locations in 1-D.}

\item{cluster_on_indicators}{Default: FALSE. Logical flag for whether indicator
variables should be used for clustering knot locations.}

\item{make_partition_list}{Default: NULL. Optional list allowing direct specification
of custom partition assignments. The \code{make_partition_list} returned by one
model can be supplied here to reuse knot locations.}

\item{previously_tuned_penalties}{Default: NULL. Optional list of pre-computed penalty
components from a previous model fit.}

\item{smoothing_spline_penalty}{Default: NULL. Optional custom smoothing spline penalty
matrix.}

\item{opt}{Default: TRUE. Logical switch controlling automatic penalty optimization.}

\item{use_custom_bfgs}{Default: TRUE. Selects between a native damped-BFGS
implementation with closed-form gradients or base R's BFGS with finite-difference
gradients. The native path is usually faster, while the finite-difference
fallback can be preferable when tuning under exact LOO and the leverage
derivative is numerically noisy.}

\item{delta}{Default: NULL. Numeric pseudocount for stabilizing optimization in
non-identity link function scenarios.}

\item{tol}{Default: \code{10*sqrt(.Machine$double.eps)}. Numeric convergence tolerance.}

\item{tuning_criterion}{Default: \code{"loo"}. Character scalar selecting the
tuning criterion. Use \code{"loo"} for exact leave-one-out on the transformed
tuning problem, or \code{"gcv"} for the generalized cross-validation
criterion. The LOO path computes the needed hat-matrix diagonal exactly
from blockwise constrained-\eqn{\mathbf{G}} quantities, without explicitly
forming the full projection matrix or full hat matrix. In empirical
diagnostics, the observation-wise derivative of the LOO leverage term can
be numerically delicate even when the overall tuning criterion and fitted
penalties remain well behaved; users who prefer a more conservative
optimization path can set \code{use_custom_bfgs = FALSE}. Shared wiggle
and flat tuning directions are differentiated directly, while optional
predictor- and partition-specific penalties continue to use a lower-cost
ratio approximation. For very large samples, generalized cross-validation
is often the more practical choice; as a rough guideline, \code{"gcv"}
is recommended once the sample size is above about 250,000.}

\item{gcv_gamma}{Default: 1.4. Numeric scalar, at least 1, used only when
\code{tuning_criterion = "gcv"}. Multiplies the effective degrees of freedom
in the GCV denominator during automatic penalty tuning. It is accepted but
ignored when \code{tuning_criterion = "loo"}.}

\item{initial_wiggle}{Default: \code{c(2e-12, 2e-7, 2e-4, 0.2)}. Numeric vector of
initial grid points for wiggle penalty optimization, on the raw (non-negative) scale.}

\item{initial_flat}{Default: \code{c(0.5, 5)}. Numeric vector of initial grid points
for ridge penalty optimization, on the raw scale (ratio of ridge to wiggle).}

\item{wiggle_penalty}{Default: 2e-7. Numeric penalty on the integrated squared second
derivative, governing function smoothness.}

\item{flat_ridge_penalty}{Default: 0.5. Numeric flat ridge penalty for intercepts and
linear terms only. Multiplied by \code{wiggle_penalty} to obtain total ridge penalty.}

\item{unique_penalty_per_partition}{Default: TRUE. Logical flag allowing penalty
magnitude to differ across partitions.}

\item{unique_penalty_per_predictor}{Default: TRUE. Logical flag allowing penalty
magnitude to differ between predictors.}

\item{meta_penalty}{Default: 1e-8. Numeric regularization coefficient for
predictor- and partition-specific penalties during tuning. On the raw scale,
the implemented meta-penalty shrinks these penalty multipliers toward 1; the
wiggle penalty receives only a tiny stabilizing penalty by default.}

\item{predictor_penalties}{Default: NULL. Optional vector of custom penalties per
predictor, on the raw (positive) scale.}

\item{partition_penalties}{Default: NULL. Optional vector of custom penalties per
partition, on the raw (positive) scale.}

\item{include_quadratic_terms}{Default: TRUE. Logical switch to include squared
predictor terms.}

\item{include_cubic_terms}{Default: TRUE. Logical switch to include cubic predictor
terms.}

\item{include_quartic_terms}{Default: NULL. Includes quartic terms; when NULL, set to
FALSE for single predictor and TRUE otherwise. Highly recommended for multi-predictor
models to avoid over-specified constraints.}

\item{include_2way_interactions}{Default: TRUE. Logical switch for linear two-way
interactions.}

\item{include_3way_interactions}{Default: TRUE. Logical switch for three-way
interactions.}

\item{include_quadratic_interactions}{Default: FALSE. Logical switch for
linear-quadratic interaction terms.}

\item{offset}{Default: Empty vector. Column indices/names to include as offsets.
Coefficients for offset terms are automatically constrained to 1.}

\item{just_linear_with_interactions}{Default: NULL. Integer or character vector
specifying predictors to retain as linear terms while still allowing
interactions.}

\item{just_linear_without_interactions}{Default: NULL. Integer or character
vector specifying predictors to retain only as linear terms without
interactions. Eligible for blockfitting.}

\item{exclude_interactions_for}{Default: NULL. Integer or character vector of
predictors to exclude from all interaction terms.}

\item{exclude_these_expansions}{Default: NULL. Character vector of basis expansions to
exclude. Named columns of data, or in the form \code{"_1_"}, \code{"_2_"},
\code{"_1_x_2_"}, \code{"_2_^2"} etc.}

\item{custom_basis_fxn}{Default: NULL. Optional user-defined function for custom basis
expansions. See \code{\link{get_polynomial_expansions}}.}

\item{include_constrain_fitted}{Default: TRUE. Logical switch to constrain fitted
values at knot points.}

\item{include_constrain_first_deriv}{Default: TRUE. Logical switch to constrain first
derivatives at knot points.}

\item{include_constrain_second_deriv}{Default: TRUE. Logical switch to constrain second
derivatives at knot points.}

\item{include_constrain_interactions}{Default: TRUE. Logical switch to constrain
interaction terms at knot points.}

\item{cl}{Default: NULL. Parallel processing cluster object
(use \code{parallel::makeCluster()}).}

\item{chunk_size}{Default: NULL. Integer specifying custom chunk size for parallel
processing.}

\item{parallel_eigen}{Default: TRUE. Logical flag for parallel eigenvalue decomposition.}

\item{parallel_trace}{Default: FALSE. Logical flag for parallel trace computation.}

\item{parallel_aga}{Default: FALSE. Logical flag for parallel \eqn{\mathbf{G}} and
\eqn{\mathbf{A}} matrix operations.}

\item{parallel_matmult}{Default: FALSE. Logical flag for parallel block-diagonal matrix
multiplication.}

\item{parallel_unconstrained}{Default: TRUE. Logical flag for parallel unconstrained
MLE for non-identity-link-Gaussian models.}

\item{parallel_find_neighbors}{Default: FALSE. Logical flag for parallel neighbor
identification.}

\item{parallel_penalty}{Default: FALSE. Logical flag for parallel penalty matrix
construction.}

\item{parallel_make_constraint}{Default: FALSE. Logical flag for parallel constraint
matrix generation.}

\item{unconstrained_fit_fxn}{Default: \code{\link{unconstrained_fit_default}}.
Custom function for fitting unconstrained models per partition.}

\item{keep_weighted_Lambda}{Default: FALSE. Logical flag to retain GLM weights in
penalty constraints using Tikhonov parameterization. Advised for non-canonical GLMs.}

\item{iterate_tune}{Default: TRUE. Logical switch for iterative optimization during
penalty tuning.}

\item{iterate_final_fit}{Default: TRUE. Logical switch for iterative optimization in
final model fitting.}

\item{blockfit}{Default: TRUE. Logical switch for backfitting with mixed spline and
non-interactive linear terms. Requires flat columns, \code{K > 0}, and no active
correlation structure. Falls back to \code{get_B} on failure.}

\item{qp_score_function}{Default: \eqn{\mathbf{X}^{\top}(\mathbf{y} - \boldsymbol{\mu})}.
Score function for quadratic programming, blockfit, and GEE formulations.
Accepts arguments \code{"X, y, mu, order_list, dispersion, VhalfInv,
observation_weights, ..."}.}

\item{qp_observations}{Default: NULL. Numeric vector of observation indices
at which built-in QP constraints are evaluated. Useful for reducing the
size of the constrained system.}

\item{qp_Amat}{Default: NULL. Optional pre-built QP constraint matrix.
In the current pipeline its presence marks QP handling as active, but the
built-in constructor does not merge it into the assembled constraint set;
use \code{qp_Amat_fxn} for custom assembled constraints.}

\item{qp_bvec}{Default: NULL. Optional pre-built QP right-hand side paired
with \code{qp_Amat}. Like \code{qp_Amat}, it is currently treated as an
advanced placeholder rather than merged into the built-in constructor.}

\item{qp_meq}{Default: 0. Optional number of equality constraints paired
with \code{qp_Amat}. Like \code{qp_Amat}, it is currently treated as an
advanced placeholder rather than merged into the built-in constructor.}

\item{qp_positive_derivative}{Default: FALSE. Constrain function to have positive first derivatives. Accepts: \code{FALSE} (no constraint), \code{TRUE} (all predictors), or a character/integer vector naming specific predictor variables to constrain. For example, \code{qp_positive_derivative = "Dose"} constrains only the Dose variable, while \code{qp_positive_derivative = c(1, 3)} constrains columns 1 and 3 of the predictor matrix.}

\item{qp_negative_derivative}{Default: FALSE. Constrain function to have negative first derivatives. Same input types as \code{qp_positive_derivative}. Can be used simultaneously with \code{qp_positive_derivative} on different variables.}

\item{qp_positive_2ndderivative}{Default: FALSE. Constrain function to have positive (convex) second derivatives. Same input types as \code{qp_positive_derivative}.}

\item{qp_negative_2ndderivative}{Default: FALSE. Constrain function to have negative (concave) second derivatives. Same input types as \code{qp_positive_derivative}.}

\item{qp_monotonic_increase}{Default: FALSE. Logical only. Constrain fitted values to be monotonically increasing in observation order.}

\item{qp_monotonic_decrease}{Default: FALSE. Logical only. Constrain fitted values to be monotonically decreasing in observation order.}

\item{qp_range_upper}{Default: NULL. Numeric upper bound for constrained fitted values.}

\item{qp_range_lower}{Default: NULL. Numeric lower bound for constrained fitted values.}

\item{qp_Amat_fxn}{Default: NULL. Custom function generating Amat.}

\item{qp_bvec_fxn}{Default: NULL. Custom function generating bvec.}

\item{qp_meq_fxn}{Default: NULL. Custom function generating meq.}

\item{constraint_values}{Default: \code{cbind()}. Optional matrix encoding
nonzero equality targets paired with \code{constraint_vectors}. When left
empty, added equality constraints are treated as homogeneous.}

\item{constraint_vectors}{Default: \code{cbind()}. Optional matrix of
user-supplied equality-constraint vectors, appended to the internally
generated smoothness constraints.}

\item{return_G}{Default: TRUE. Logical switch to return the unscaled unconstrained
variance-covariance matrix \eqn{\mathbf{G}}.}

\item{return_Ghalf}{Default: TRUE. Logical switch to return
\eqn{\mathbf{G}^{1/2}}.}

\item{return_U}{Default: TRUE. Logical switch to return the constraint projection
matrix \eqn{\mathbf{U}}.}

\item{estimate_dispersion}{Default: TRUE. Logical flag to estimate dispersion after
fitting.}

\item{unbias_dispersion}{Default: NULL. Logical switch to multiply dispersion by
\eqn{N/(N - \mathrm{trace}(\mathbf{H}))}. When NULL, set to TRUE for Gaussian
identity link and FALSE otherwise.}

\item{return_varcovmat}{Default: TRUE. Logical switch to return the variance-covariance
matrix of estimated coefficients. Needed for Wald inference.}

\item{exact_varcovmat}{Default: FALSE. Logical switch to replace the default
asymptotic (Bayesian posterior) variance-covariance matrix with the exact
frequentist variance-covariance matrix of the constrained estimator. The
asymptotic version uses the Hessian of
the penalized log-likelihood:
\eqn{\tilde{\sigma}^{2}\mathbf{U}\mathbf{G}\mathbf{U}^{\top}}.
The exact version additionally corrects for the penalty's contribution as a shrinkage
prior, giving:
\deqn{\tilde{\sigma}^{2}\mathbf{U}\mathbf{G}\mathbf{U}^{\top}
      - \tilde{\sigma}^{2}\mathbf{U}\mathbf{G}\boldsymbol{\Lambda}\mathbf{G}\mathbf{U}^{\top}}
When a correlation structure is present (\code{VhalfInv} non-NULL),
\eqn{\mathbf{G}_{\mathrm{correct}}} replaces the block-diagonal \eqn{\mathbf{G}}.
For Gaussian identity link (with or without correlation structure), the result is
the exact variance-covariance matrix of the constrained estimate.
The returned object still stores the result in \code{varcovmat}. Requires
\code{return_varcovmat = TRUE}.}

\item{return_lagrange_multipliers}{Default: FALSE. Logical switch to return the
Lagrangian multiplier vector.}

\item{custom_penalty_mat}{Default: NULL. Optional \eqn{p \times p} custom penalty
matrix for individual partitions, replacing the default ridge on linear/intercept
terms. Run with \code{dummy_fit = TRUE} first to inspect expansion structure.}

\item{cluster_args}{Default: \code{c(custom_centers = NA, nstart = 10)}. Named
vector of arguments controlling clustering. If the first argument is not
\code{NA}, it is treated as custom cluster centers (typically an
\eqn{(K+1) \times q} matrix). Otherwise, default k-means is used.}

\item{dummy_dividor}{Default: 0.00000000000000000000012345672152894. Small numeric
constant to prevent division by zero.}

\item{dummy_adder}{Default: 0.000000000000000002234567210529. Small numeric constant
to prevent division by zero.}

\item{verbose}{Default: FALSE. Logical flag to print general progress messages.}

\item{verbose_tune}{Default: FALSE. Logical flag to print detailed progress during
penalty tuning.}

\item{dummy_fit}{Default: FALSE. Runs the full pipeline but sets coefficients to zero,
allowing inspection of design matrix structure, penalty matrices, and partitioning.
Replaces the deprecated \code{expansions_only} argument.}

\item{auto_encode_factors}{Default: TRUE. Logical switch to automatically one-hot encode
factor or character variables when using the formula interface.}

\item{observation_weights}{Default: NULL. Numeric vector of observation-specific
weights for generalized least squares estimation.}

\item{do_not_cluster_on_these}{Default: \code{c()}. Predictor columns to exclude from
clustering. Accepts numeric column indices or character column names.}

\item{neighbor_tolerance}{Default: \code{1 + 1e-8}. Numeric tolerance for determining
neighboring partitions using k-means clustering. Intended for internal use.}

\item{null_constraint}{Default: NULL. Alternative parameterization for a
nonzero equality target when \code{constraint_vectors} is supplied and
\code{constraint_values} is left empty.}

\item{critical_value}{Default: \code{qnorm(1-0.05/2)}. Numeric critical value for
Wald confidence intervals.}

\item{data}{Default: NULL. Optional data frame for formula-based model specification.}

\item{weights}{Default: NULL. Alias for \code{observation_weights}.}

\item{no_intercept}{Default: FALSE. Logical flag to constrain intercept to 0.
Formulas with \code{"0+"} set this to TRUE automatically.}

\item{correlation_id, spacetime}{Default: NULL. N-length vector and N-row matrix of
cluster ids and longitudinal/spatial variables, respectively.}

\item{correlation_structure}{Default: NULL. Native implementations: \code{"exchangeable"},
\code{"spatial-exponential"}, \code{"squared-exponential"}, \code{"ar(1)"},
\code{"spherical"}, \code{"gaussian-cosine"}, \code{"gamma-cosine"},
\code{"matern"}, and aliases. Estimated via REML.}

\item{VhalfInv}{Default: NULL. Fixed custom \eqn{N \times N} square-root-inverse
covariance matrix \eqn{\mathbf{V}^{-1/2}}. Triggers GLS with known covariance.
Post-fit inference recomputed from whitened Gram matrices.}

\item{Vhalf}{Default: NULL. Fixed custom \eqn{N \times N} square-root covariance
\eqn{\mathbf{V}^{1/2}}. Computed as inverse of \code{VhalfInv} if not supplied.}

\item{VhalfInv_fxn}{Default: NULL. Parametric function for \eqn{\mathbf{V}^{-1/2}};
takes single numeric vector \code{"par"}, returns \eqn{N \times N} matrix.
Optimized via BFGS when \code{VhalfInv_par_init} is provided.}

\item{Vhalf_fxn}{Default: NULL. Optional function for efficient computation of
\eqn{\mathbf{V}^{1/2}} from the same parameter vector used by
\code{VhalfInv_fxn}. When omitted, \code{Vhalf} is obtained by explicit
matrix inversion of \code{VhalfInv}.}

\item{VhalfInv_par_init}{Default: \code{c()}. Initial parameter values for
\code{VhalfInv_fxn} optimization, on unbounded transformed scale.}

\item{REML_grad}{Default: NULL. Function for the gradient of the negative REML (or
custom loss) with respect to the parameters of \code{VhalfInv_fxn}. Takes
\code{"par"}, \code{"model_fit"}, and \code{"..."}.}

\item{custom_VhalfInv_loss}{Default: NULL. Alternative to negative REML for the
correlation parameter objective function. Takes \code{"par"}, \code{"model_fit"},
and \code{"..."}.}

\item{VhalfInv_logdet}{Default: NULL. Function for efficient \eqn{\log|\mathbf{V}^{-1/2}|}
computation. Takes same \code{"par"} as \code{VhalfInv_fxn}.}

\item{include_warnings}{Default: TRUE. Logical switch to control display of warnings.}

\item{penalty_args}{Default: NULL. Optional named list grouping penalty-related
arguments. See section "Grouped Argument Lists".}

\item{tuning_args}{Default: NULL. Optional named list grouping tuning-related
arguments.}

\item{expansion_args}{Default: NULL. Optional named list grouping basis expansion
arguments.}

\item{constraint_args}{Default: NULL. Optional named list grouping constraint arguments.}

\item{qp_args}{Default: NULL. Optional named list grouping quadratic programming
arguments.}

\item{parallel_args}{Default: NULL. Optional named list grouping parallel processing
arguments.}

\item{covariance_args}{Default: NULL. Optional named list grouping correlation
structure arguments.}

\item{return_args}{Default: NULL. Optional named list grouping return-control
arguments.}

\item{glm_args}{Default: NULL. Optional named list grouping GLM customization
arguments.}

\item{...}{Additional arguments passed to the unconstrained model fitting function.}
}
\value{
A list of class \code{"lgspline"} containing model components:
\describe{
  \item{y}{Original response vector.}
  \item{ytilde}{Fitted/predicted values on the scale of the response.}
  \item{X}{List of design matrices \eqn{\mathbf{X}_{k}} for each partition k, containing basis expansions including intercept, linear, quadratic, cubic, and interaction terms as specified. Returned on the unstandardized scale.}
  \item{A}{Constraint matrix \eqn{\mathbf{A}} encoding smoothness constraints at knot points and any user-specified linear constraints. Only a linearly independent subset of columns is retained (via pivoted QR decomposition).}
  \item{B}{List of fitted coefficients \eqn{\boldsymbol{\beta}_{k}} for each partition k on the original, unstandardized scale of the predictors and response.}
  \item{B_raw}{List of fitted coefficients for each partition on the predictor-and-response standardized scale.}
  \item{K}{Number of interior knots with one predictor (number of partitions minus 1 with > 1 predictor).}
  \item{p}{Number of basis expansions of predictors per partition.}
  \item{q}{Number of predictor variables.}
  \item{P}{Total number of coefficients (\eqn{p \times (K+1)}).}
  \item{N}{Number of observations.}
  \item{penalties}{List containing optimized penalty matrices and components:
    \itemize{
      \item Lambda: Combined penalty matrix (\eqn{\boldsymbol{\Lambda}}), includes \eqn{\mathbf{L}_{\mathrm{predictor\_list}}} contributions but not \eqn{\mathbf{L}_{\mathrm{partition\_list}}}.
      \item L1: Smoothing spline penalty matrix (\eqn{\mathbf{L}_{1}}).
      \item L2: Ridge penalty matrix (\eqn{\mathbf{L}_{2}}).
      \item L_predictor_list: Predictor-specific penalty matrices (\eqn{\mathbf{L}_{\mathrm{predictor\_list}}}).
      \item L_partition_list: Partition-specific penalty matrices (\eqn{\mathbf{L}_{\mathrm{partition\_list}}}).
    }
  }
  \item{knot_scale_transf}{Function for transforming predictors to standardized scale used for knot placement.}
  \item{knot_scale_inv_transf}{Function for transforming standardized predictors back to original scale.}
  \item{knots}{Matrix of knot locations on original unstandardized predictor scale for one predictor.}
  \item{partition_codes}{Vector assigning observations to partitions.}
  \item{partition_bounds}{Vector or matrix specifying the boundaries between partitions.}
  \item{knot_expand_function}{Internal function for expanding data according to partition structure.}
  \item{predict}{Function for generating predictions on new data. For multi-predictor models, \code{take_first_derivatives = TRUE, take_second_derivatives} returns derivatives as a named list of components per predictor variable, rather than a concatenated vector. When \code{new_predictors} contains columns not present in the data, extraneous columns are silently dropped before prediction.}
  \item{assign_partition}{Function for assigning new observations to partitions.}
  \item{family}{GLM family object specifying the error distribution and link function.}
  \item{estimate_dispersion}{Logical indicating whether dispersion parameter was estimated.}
  \item{unbias_dispersion}{Logical indicating whether dispersion estimates should be unbiased.}
  \item{backtransform_coefficients}{Function for converting standardized coefficients to original scale.}
  \item{forwtransform_coefficients}{Function for converting coefficients to standardized scale.}
  \item{mean_y, sd_y}{Mean and standard deviation of response if standardized.}
  \item{og_order}{Original ordering of observations before partitioning.}
  \item{order_list}{List containing observation indices for each partition.}
  \item{constraint_values, constraint_vectors}{Matrices specifying linear equality constraints if provided.}
  \item{make_partition_list}{List containing partition information for > 1-D cases.}
  \item{expansion_scales}{Vector of scaling factors used for standardizing basis expansions.}
  \item{take_derivative, take_interaction_2ndderivative}{Functions for computing derivatives of basis expansions.}
  \item{get_all_derivatives_insample}{Function for computing all derivatives on training data.}
  \item{numerics}{Indices of numeric predictors used in basis expansions.}
  \item{power1_cols, power2_cols, power3_cols, power4_cols}{Column indices for linear through quartic terms.}
  \item{quad_cols}{Column indices for all quadratic terms (including interactions).}
  \item{interaction_single_cols, interaction_quad_cols}{Column indices for linear-linear and linear-quadratic interactions.}
  \item{triplet_cols}{Column indices for three-way interactions.}
  \item{nonspline_cols}{Column indices for terms excluded from spline expansion.}
  \item{return_varcovmat}{Logical indicating whether variance-covariance matrix was computed.}
  \item{raw_expansion_names}{Names of basis expansion terms.}
  \item{std_X, unstd_X}{Functions for standardizing/unstandardizing design matrices.}
  \item{parallel_cluster_supplied}{Logical indicating whether a parallel cluster was supplied.}
  \item{weights}{Original observation weights on the data scale. When no
    weights were supplied, this is a vector of ones.}
  \item{G}{List of unscaled partition-wise information inverses
    \eqn{\mathbf{G}_{k}} if \code{return_G = TRUE}. These are the blockwise
    quantities stored on the fitting scale; correlation-aware trace,
    posterior, and variance calculations additionally use dense GLS analogues
    internally when needed.}
  \item{Ghalf}{List of \eqn{\mathbf{G}_{k}^{1/2}} matrices if
    \code{return_Ghalf = TRUE}. As with \code{G}, dense GLS square-root
    factors may also be constructed internally for correlation-aware
    post-fit calculations.}
  \item{U}{Constraint projection matrix \eqn{\mathbf{U}} if \code{return_U = TRUE}. For K=0 and no constraints, returns identity. Otherwise, returns \eqn{\mathbf{U} = \mathbf{I} - \mathbf{G}\mathbf{A}(\mathbf{A}^{\top}\mathbf{G}\mathbf{A})^{-1}\mathbf{A}^{\top}}. Used for computing the variance-covariance matrix \eqn{\sigma^{2}\mathbf{U}\mathbf{G}}.}
  \item{sigmasq_tilde}{Estimated (or fixed) dispersion parameter
    \eqn{\tilde{\sigma}^{2}}. For Gaussian identity fits without correlation,
    this is the weighted mean squared residual with optional bias correction.
    When \code{VhalfInv} is non-\code{NULL}, Gaussian-identity residuals are
    whitened before this calculation.}
  \item{trace_XUGX}{Effective degrees of freedom (\eqn{\mathrm{trace}(\mathbf{X}\mathbf{U}\mathbf{G}\mathbf{X}^{\top})}), where \eqn{\mathbf{X}\mathbf{U}\mathbf{G}\mathbf{X}^{\top}} serves as the "hat" matrix. When \code{VhalfInv} is non-\code{NULL}, computed as \eqn{\|\mathbf{V}^{-1/2}\mathbf{X}\mathbf{U}\mathbf{G}_{\mathrm{correct}}^{1/2}\|_{F}^{2}} using the full penalized GLS information.}
  \item{varcovmat}{Variance-covariance matrix of coefficient estimates if \code{return_varcovmat = TRUE}. Computed as \eqn{\sigma^{2}(\mathbf{U}\mathbf{G}^{1/2})(\mathbf{U}\mathbf{G}^{1/2})^{\top}} for numerical stability. When \code{VhalfInv} is non-\code{NULL}, uses the full \eqn{\mathbf{G}_{\mathrm{correct}}^{1/2}} in place of the block-diagonal \eqn{\mathbf{G}^{1/2}}.}
  \item{lagrange_multipliers}{Vector of Lagrangian multipliers if \code{return_lagrange_multipliers = TRUE}. For equality-only fits these correspond to the active columns of \eqn{\mathbf{A}}; when quadratic-programming constraints are active they are taken directly from \code{solve.QP} and therefore refer to the combined equality/inequality constraint system. \code{NULL} if no constraints are active (\eqn{\mathbf{A}} is \code{NULL} or \code{K == 0}).}
  \item{VhalfInv}{The \eqn{\mathbf{V}^{-1/2}} matrix used for implementing correlation structures, if specified.}
  \item{VhalfInv_fxn, Vhalf_fxn, VhalfInv_logdet, REML_grad}{Functions for generating \eqn{\mathbf{V}^{-1/2}}, \eqn{\mathbf{V}^{1/2}}, \eqn{\log|\mathbf{V}^{-1/2}|}, and gradient of REML if provided.}
  \item{VhalfInv_params_estimates}{Vector of estimated correlation parameters when using \code{VhalfInv_fxn}.}
  \item{VhalfInv_params_vcov}{Approximate variance-covariance matrix of estimated correlation parameters from BFGS optimization.}
  \item{wald_univariate}{Function for computing univariate Wald statistics and confidence intervals. Returns an S3 object of class \code{"wald_lgspline"} with dedicated \code{print}, \code{summary}, \code{plot}, \code{coef}, and \code{confint} methods. The \code{print} method uses \code{printCoefmat()} for standard R coefficient table formatting with significance stars.}
  \item{critical_value}{Critical value used for confidence interval construction.}
  \item{generate_posterior}{Function for drawing from the posterior distribution of coefficients. When \code{VhalfInv} is non-\code{NULL}, draws are from the correct joint posterior \eqn{\mathbf{U}\mathbf{G}_{\mathrm{correct}}^{1/2}\mathbf{z}} using the full penalized GLS information, reflecting cross-partition posterior covariance induced by off-diagonal blocks of \eqn{\mathbf{V}^{-1/2}}.}
  \item{find_extremum}{Function for optimizing the fitted function. Accepts both numeric column indices and character column names for \code{vars}. When \code{select_vars_fl = TRUE}, L-BFGS-B bounds are correctly subsetted to the optimized variables.}
  \item{plot}{Function for visualizing fitted curves.}
  \item{quadprog_list}{List containing quadratic programming components if applicable.}
  \item{.fit_call_args}{List containing the arguments passed to
    \code{\link{lgspline}}.}
}

The returned object has class \code{"lgspline"} and provides comprehensive tools for
model interpretation, inference, prediction, and visualization. All
coefficients and predictions can be transformed between standardized and
original scales using the provided transformation functions. The object includes
both frequentist and Bayesian inference capabilities through Wald statistics
and posterior sampling. S3 methods \code{\link{logLik.lgspline}} and
\code{\link{confint.lgspline}} are available for standard log-likelihood
extraction and confidence interval computation, respectively.
Advanced customization options are available for
analyzing arbitrarily complex study designs.
}
\description{
A comprehensive software package for fitting a variant of smoothing splines
as a constrained optimization problem, avoiding the need to algebraically
disentangle a spline basis after fitting, and allowing for interpretable
interactions and non-spline effects to be included.

\code{lgspline} fits piecewise polynomial regression splines constrained to be smooth where
they meet, penalized by the squared, integrated, second-derivative of the
estimated function with respect to predictors, using a monomial basis.

The method of Lagrangian multipliers is used to derive a polynomial regression spline
that enforces the following smoothing constraints:
\itemize{
  \item Equivalent fitted values at knots
  \item Equivalent first derivatives at knots, with respect to predictors
  \item Equivalent second derivatives at knots, with respect to predictors
}

The coefficients are penalized by a closed-form of the traditional
cubic smoothing spline penalty, as well as tunable modifications that allow
for unique penalization of multiple predictors and partitions.

This package supports model fitting for multiple spline and non-spline effects,
GLM families, Weibull accelerated failure time (AFT) models,
Cox proportional-Hazards models, negative-binomial regression,
arbitrary correlation structures, shape constraints, and extensive
customization for user-defined models and constraints.

In addition, parallel processing capabilities and comprehensive
tools for visualization, frequentist, and Bayesian inference are provided.
}
\details{
A flexible and interpretable implementation of smoothing splines including:
\itemize{
  \item Multiple predictors and interaction terms
  \item Various GLM families and link functions
  \item Correlation structures for longitudinal/clustered data
  \item Shape constraints via quadratic programming
  \item Parallel computation for large datasets
  \item Comprehensive inference tools
}
}
\section{Response and Predictor Setup}{

These arguments control the primary data inputs and the initial
standardization steps applied before knot placement and fitting.
}

\section{GLM Customization}{

These options let you override the default GLM working-weight,
dispersion, and partition-wise unconstrained fitting behavior.
}

\section{Knots and Partitioning}{

These arguments determine how the predictor space is partitioned and how
knot locations are chosen or reused.
}

\section{Penalty}{

These arguments configure the smoothing penalty itself and the optional
tuning procedure, using exact leave-one-out by default and GCV optionally.
}

\section{Basis Expansions}{

These arguments control which polynomial and interaction terms are included
in the partition-specific design matrices.
}

\section{Constraints}{

These arguments govern the smoothness equalities and any additional user
supplied linear equality constraints.
}

\section{Quadratic Programming}{

These arguments activate built-in or custom inequality constraints handled
through quadratic programming.
}

\section{Parallel Processing}{

These arguments control which computational subroutines may run in parallel
and how work is chunked across cluster workers.
}

\section{Tuning Control}{

These options control iterative updates during penalty tuning and the final
constrained fit.
}

\section{Return Control}{

These arguments determine which intermediate matrices and inferential
quantities are retained in the returned fit object.
}

\section{Correlation Structures}{

These arguments enable built-in or custom working-correlation structures for
longitudinal, clustered, or spatially indexed responses. In the notation
used throughout \code{\link{Details}}, the correlated penalized
information is written as
\eqn{\mathbf{G}^{-1} =
\mathbf{G}_{\mathrm{on}}^{-1} + \mathbf{G}_{\mathrm{off}}^{-1}}.
When the cross-partition part is low rank, the internal Woodbury helpers
factor
\eqn{\mathbf{G}_{\mathrm{off}}^{-1} =
\mathbf{E}\mathbf{J}\mathbf{E}^{\top}}
and accelerate the correlated solve without changing the final estimator.
}

\section{Grouped Argument Lists}{

For convenience, related arguments can be bundled into named lists.
When a grouped argument is non-NULL, its entries overwrite the corresponding
individual arguments. Individual arguments remain available for backward compatibility.

\describe{
  \item{\code{penalty_args}}{Groups: \code{wiggle_penalty},
    \code{flat_ridge_penalty}, \code{unique_penalty_per_partition},
    \code{unique_penalty_per_predictor}, \code{meta_penalty},
    \code{predictor_penalties}, \code{partition_penalties},
    \code{custom_penalty_mat}, \code{previously_tuned_penalties},
    \code{smoothing_spline_penalty}.}
  \item{\code{tuning_args}}{Groups: \code{opt},
    \code{use_custom_bfgs}, \code{delta}, \code{tol},
    \code{tuning_criterion},
    \code{gcv_gamma},
    \code{initial_wiggle}, \code{initial_flat},
    \code{iterate_tune}, \code{iterate_final_fit}.}
  \item{\code{expansion_args}}{Groups: \code{include_quadratic_terms},
    \code{include_cubic_terms}, \code{include_quartic_terms},
    \code{include_2way_interactions}, \code{include_3way_interactions},
    \code{include_quadratic_interactions},
    \code{just_linear_with_interactions},
    \code{just_linear_without_interactions},
    \code{exclude_interactions_for}, \code{exclude_these_expansions},
    \code{custom_basis_fxn}, \code{offset}.}
  \item{\code{constraint_args}}{Groups:
    \code{include_constrain_fitted},
    \code{include_constrain_first_deriv},
    \code{include_constrain_second_deriv},
    \code{include_constrain_interactions},
    \code{constraint_values}, \code{constraint_vectors},
    \code{no_intercept}.}
  \item{\code{qp_args}}{Groups all \code{qp_*} arguments.}
  \item{\code{parallel_args}}{Groups: \code{cl},
    \code{chunk_size}, and all \code{parallel_*} flags.}
  \item{\code{covariance_args}}{Groups: \code{correlation_id},
    \code{spacetime}, \code{correlation_structure},
    \code{VhalfInv}, \code{Vhalf}, \code{VhalfInv_fxn},
    \code{Vhalf_fxn}, \code{VhalfInv_par_init},
    \code{REML_grad}, \code{custom_VhalfInv_loss},
    \code{VhalfInv_logdet}.}
  \item{\code{return_args}}{Groups: \code{return_G},
    \code{return_Ghalf}, \code{return_U},
    \code{estimate_dispersion}, \code{unbias_dispersion},
    \code{return_varcovmat}, \code{exact_varcovmat},
    \code{return_lagrange_multipliers}.}
  \item{\code{glm_args}}{Groups: \code{glm_weight_function},
    \code{schur_correction_function},
    \code{need_dispersion_for_estimation},
    \code{dispersion_function}, \code{unconstrained_fit_fxn},
    \code{keep_weighted_Lambda}.}
}
}

\section{Miscellaneous}{

These remaining arguments affect inference defaults, numerical safeguards,
verbosity, and developer-oriented diagnostics.
}

\examples{

## ## ## ## Simple Examples ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
## Simulate some data, fit using default settings without tuning, and plot
set.seed(1234)
t <- runif(2500, -10, 10)
y <- 2*sin(t) + -0.06*t^2 + rnorm(length(t))
model_fit <- lgspline(t, y, opt = FALSE)
plot(t, y, main = 'Observed Data vs. Fitted Function, Colored by Partition',
     ylim = c(-10, 10))
plot(model_fit, add = TRUE)

\donttest{
## Repeat using logistic regression, with univariate inference shown
# and alternative function call
y <- rbinom(length(y), 1, 1/(1+exp(-std(y))))
df <- data.frame(t = t, y = y)
model_fit <- lgspline(y ~ spl(t),
                      df,
                      family = binomial())
plot(t, y, main = 'Observed Data vs Fitted Function with Formulas and Derivatives',
  ylim = c(-0.5, 1.05), cex.main = 0.8)
plot(model_fit,
     show_formulas = TRUE,
     text_size_formula = 0.65,
     legend_pos = 'bottomleft',
     legend_args = list(y.intersp = 1.1),
     add = TRUE)
## Notice how the coefficients match the formula, and expansions are
# homogenous across partitions without reparameterization
print(summary(model_fit))

## Overlay first and second derivatives of fitted function respectively
derivs <- predict(model_fit,
                  new_predictors = sort(t),
                  take_first_derivatives = TRUE,
                  take_second_derivatives = TRUE)
points(sort(t), derivs$first_deriv, col = 'gold', type = 'l')
points(sort(t), derivs$second_deriv, col = 'goldenrod', type = 'l')
legend('bottomright',
       col = c('gold','goldenrod'),
       lty = 1,
       legend = c('First Derivative', 'Second Derivative'))

## Simple 2D example - including a non-spline effect
z <- seq(-2, 2, length.out = length(y))
df <- data.frame(Predictor1 = t,
                 Predictor2 = z,
                 Response = sin(y)+0.1*z)
model_fit <- lgspline(Response ~ spl(Predictor1) + Predictor1*Predictor2,
                      df)

## Notice, while spline effects change over partitions,
# interactions and non-spline effects are constrained to remain the same
coefficients <- Reduce('cbind', coef(model_fit))
colnames(coefficients) <- paste0('Partition ', 1:(model_fit$K+1))
print(coefficients)

## One or two variables can be selected for plotting at a time
# even when >= 3 predictors are present
plot(model_fit,
      custom_title = 'Marginal Relationship of Predictor 1 and Response',
      vars = 'Predictor1',
      custom_response_lab = 'Response',
      show_formulas = TRUE,
      legend_pos = 'bottomright',
      digits = 4,
      text_size_formula = 0.5)

## 3D plots are implemented as well, retaining closed-formulas
my_plot <- plot(model_fit,
                show_formulas = TRUE,
                custom_response_lab = 'Response')
my_plot


## ## ## ## More Detailed 1D Example ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
## 1D data generating functions
t <- seq(-9, 9, length.out = 1000)
slinky <- function(x) {
  (50 * cos(x * 2) -2 * x^2 + (0.25 * x)^4 + 80)
}
coil <- function(x) {
  (100 * cos(x * 2) +-1.5 * x^2 + (0.1 * x)^4 +
  (0.05 * x^3) + (-0.01 * x^5) +
     (0.00002 * x^6) -(0.000001 * x^7) + 100)
}
exponential_log <- function(x) {
  unlist(c(sapply(x, function(xx) {
    if (xx <= 1) {
      100 * (exp(xx) - exp(1))
    } else {
      100 * (log(xx))
    }
  })))
}
scaled_abs_gamma <- function(x) {
  2*sqrt(gamma(abs(x)))
}

## Composite function
fxn <- function(x)(slinky(t) +
                   coil(t) +
                   exponential_log(t) +
                   scaled_abs_gamma(t))

## Bind together with random noise
dat <- cbind(t, fxn(t) + rnorm(length(t), 0, 50))
colnames(dat) <- c('t', 'y')
x <- dat[,'t']
y <- dat[,'y']

## Fit Model, 4 equivalent ways are shown below
model_fit <- lgspline(t, y, opt = FALSE)
model_fit <- lgspline(y ~ spl(t), as.data.frame(dat), opt = FALSE)
model_fit <- lgspline(response = y, predictors = t, opt = FALSE)
model_fit <- lgspline(data = as.data.frame(dat), formula = y ~ ., opt = FALSE)

# This is not valid: lgspline(y ~ ., t)
# This is not valid: lgspline(y, data = as.data.frame(dat))
# Do not put operations in formulas, not valid: lgspline(y ~ log(t) + spl(t))

## Basic Functionality
predict(model_fit, new_predictors = rnorm(1)) # make prediction on new data
loo_vals <- suppressWarnings(head(leave_one_out(model_fit)))
loo_vals # may contain NA when leverage is too high
coef(model_fit) # extract coefficients
summary(model_fit) # model information and Wald inference
generate_posterior(model_fit) # generate draws of parameters from posterior distribution
find_extremum(model_fit, minimize = TRUE) # find the minimum of the fitted function

## Incorporate range constraints, custom knots, keep penalization identical
# across partitions
model_fit <- lgspline(y ~ spl(t),
                      unique_penalty_per_partition = FALSE,
                      custom_knots = cbind(c(-2, -1, 0, 1, 2)),
                      data = data.frame(t = t, y = y),
                      qp_range_lower = -150,
                      qp_range_upper = 150)

## Plotting the constraints and knots
plot(model_fit,
     custom_title = 'Fitted Function Constrained to Lie Between (-150, 150)',
     cex.main = 0.75)
# knot locations
abline(v = model_fit$knots)
# lower bound from quadratic program
abline(h = -150, lty = 2)
# upper bound from quadratic program
abline(h = 150, lty = 2)
# observed data
points(t, y, cex = 0.24)

## Enforce monotonic increasing constraints on fitted values
# K = 4 => 5 partitions
t <- seq(-10, 10, length.out = 100)
y <- 5*sin(t) + t + 2*rnorm(length(t))
model_fit <- lgspline(t,
                      y,
                      K = 4,
                      qp_monotonic_increase = TRUE)
plot(t, y, main = 'Monotonic Increasing Function with Respect to Fitted Values')
plot(model_fit,
     add = TRUE,
     show_formulas = TRUE,
     legend_pos = 'bottomright',
     custom_predictor_lab = 't',
     custom_response_lab = 'y')

## Posterior draws under constraint
draw <- generate_posterior(model_fit, enforce_qp_constraints = TRUE)
pr <- predict(model_fit, B_predict = draw$post_draw_coefficients)
points(t, pr, col = 'grey')

## ## ## ## 2D Example using Volcano Dataset ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
## Prep
data('volcano')
volcano_long <-
  Reduce('rbind', lapply(1:nrow(volcano), function(i){
    t(sapply(1:ncol(volcano), function(j){
      c(i, j, volcano[i,j])
    }))
  }))
colnames(volcano_long) <- c('Length', 'Width', 'Height')

## Fit, with 50 partitions
# When fitting with > 1 predictor and large K, including quartic terms
# is highly recommended, and/or dropping the second-derivative constraint.
# Otherwise, the constraints can impose all partitions to be equal, with one
# cubic function fit for all (there is not enough degrees of freedom to fit
# unique cubic functions due to the massive amount of constraints).
# Below, quartic terms are included and the constraint of second-derivative
# smoothness at knots is ignored.
model_fit <- lgspline(volcano_long[,c(1, 2)],
                      volcano_long[,3],
                      include_quadratic_interactions = TRUE,
                      K = 49,
                      opt = FALSE,
                      return_U = FALSE,
                      return_varcovmat = FALSE,
                      estimate_dispersion = TRUE,
                      return_Ghalf = FALSE,
                      return_G = FALSE,
                      include_constrain_second_deriv = FALSE,
                      unique_penalty_per_predictor = FALSE,
                      unique_penalty_per_partition = FALSE,
                      wiggle_penalty = 1e-10, # the fixed wiggle penalty
                      flat_ridge_penalty = 1e-2) # the ridge penalty / wiggle penalty

## Plotting on new data with interactive visual + formulas
new_input <- expand.grid(seq(min(volcano_long[,1]),
                             max(volcano_long[,1]),
                             length.out = 250),
                         seq(min(volcano_long[,2]),
                             max(volcano_long[,2]),
                             length.out = 250))
plot(model_fit,
     new_predictors = new_input,
     show_formulas = TRUE,
     custom_response_lab = "Height",
     custom_title = 'Volcano 3-D Map',
     digits = 2)

## Get AUC
area_under_volcano <- integrate(model_fit,
                               lower = apply(volcano_long, 2, min)[1:2],
                               upper = apply(volcano_long, 2, max)[1:2])

## ## ## ## Advanced Techniques using Trees Dataset ## ## ## ## ## ## ## ## ## ## ## ## ##
## Goal here is to introduce how lgspline works with non-canonical GLMs and
# demonstrate some custom features
data('trees')

## L1-regularization constraint function on standardized coefficients
# Bound all coefficients to be less than a certain value (l1_bound) in absolute
# magnitude such that | B^{(j)}_k | < lambda for all j = 1....p coefficients,
# and k = 1...K+1 partitions.
l1_constraint_matrix <- function(p, K) {
  ## Total number of coefficients
  P <- p * (K + 1)

  ## Create diagonal matrices for L1 constraint
  # First matrix: lamdba > -bound
  # Second matrix: -lambda > -bound
  first_diag <- diag(P)
  second_diag <- -diag(P)

  ## Combine matrices
  l1_Amat <- cbind(first_diag, second_diag)

  return(l1_Amat)
}

## Bounds absolute value of coefficients to be < l1_bound
l1_bound_vector <- function(qp_Amat,
                            scales,
                            l1_bound) {

  ## Combine matrices
  l1_bvec <- rep(-l1_bound, ncol(qp_Amat)) * c(1, scales)

  return(l1_bvec)
}

## Fit model, using predictor-response formulation, assuming
# Gamma-distributed response, and custom quadratic-programming constraints,
# with qp_score_function/glm_weight_function updated for non-canonical GLMs
# as well as quartic terms, keeping the effect of height constant across
# partitions, and 3 partitions in total. Hence, this is an advanced-usage
# case.
# You can modify this code for performing l1-regularization in general.
# For canonical GLMs, the default qp_score_function/glm_weight_function are
# correct and do not need to be changed
# (custom functionality is not needed for canonical GLMs).
model_fit <- lgspline(
  Volume ~ spl(Girth) + Height*Girth,
  data = with(trees, cbind(Girth, Height, Volume)),
  family = Gamma(link = 'log'),
  keep_weighted_Lambda = TRUE,
  glm_weight_function = function(
    mu,
    y,
    order_indices,
    family,
    dispersion,
    observation_weights,
   ...){
     rep(1/dispersion, length(y))
   },
   dispersion_function = function(
     mu,
     y,
     order_indices,
     family,
     observation_weights,
     VhalfInv,
     ...){
    mean(
      mu^2/((y-mu)^2)
    )
  }, # = biased estimate of 1/shape parameter
  need_dispersion_for_estimation = TRUE,
  unbias_dispersion = TRUE, # multiply dispersion by N/(N-trace(XUGX^{T}))
  K = 2, # 3 partitions
  opt = FALSE, # keep penalties fixed
  unique_penalty_per_partition = FALSE,
  unique_penalty_per_predictor = FALSE,
  flat_ridge_penalty = 1e-64,
  wiggle_penalty = 1e-64,
  qp_score_function = function(X, y, mu, order_list, dispersion, VhalfInv,
    observation_weights, ...){
   t(X) \%*\% diag(c(1/mu * 1/dispersion)) \%*\% cbind(y - mu)
  }, # updated score for gamma regression with log link
  qp_Amat_fxn = function(N, p, K, X, colnm, scales, deriv_fxn, ...) {
    l1_constraint_matrix(p, K)
  },
  qp_bvec_fxn = function(qp_Amat, N, p, K, X, colnm, scales, deriv_fxn, ...) {
    l1_bound_vector(qp_Amat, scales, 25)
  },
  qp_meq_fxn = function(qp_Amat, N, p, K, X, colnm, scales, deriv_fxn, ...) 0
)

## Notice, interaction effect is constant across partitions as is the effect
# of Height alone
Reduce('cbind', coef(model_fit))

## Many constraints, many coefficients, and small sample size makes inference
#  using asymptotic variance-covariance matrix untrustworthy.
#  Confidence intervals are often too wide or narrow, even for "good" fit.
#  Consider bootstrapping or alternative.
print(summary(model_fit))

## Plot results
plot(model_fit, custom_predictor_lab1 = 'Girth',
     custom_predictor_lab2 = 'Height',
     custom_response_lab = 'Volume',
     custom_title = 'Girth and Height Predicting Volume of Trees',
     show_formulas = TRUE)

## Verify magnitude of unstandardized coefficients does not exceed bound (25)
print(max(abs(unlist(model_fit$B))))

## Find height and girth where tree volume is closest to 42
# Uses custom objective that minimizes MSE discrepancy between predicted
# value and 42.
# The vanilla find_extremum function can be thought of as
# using "function(mu)mu" aka the identity function as the
# objective, where mu = "f(t)", our estimated function. The derivative is then
# d_mu = "df/dt" with respect to predictors t.
# But with more creative objectives, and since we have machinery for
# df/dt already available, we can compute gradients for (and optimize)
# arbitrary differentiable functions of our predictors too.
# For any objective, differentiate w.r.t. to mu, then multiply by d_mu to
# satisfy chain rule.
# Here, we have objective function: 0.5*(mu-42)^2
# and gradient                    : (mu-42)*d_mu
# and L-BFGS-B will be used to find the height and girth that most closely
# yields a prediction of 42 within the bounds of the observed data.
# The d_mu also takes into account link function transforms automatically
# for most common link functions, and will return warning + instructions
# on how to program the link-function derivatives otherwise.

## Custom acquisition functions for Bayesian optimization could be coded here.
find_extremum(
  model_fit,
  minimize = TRUE,
  custom_objective_function = function(mu, sigma, ybest, ...){
    0.5*(mu - 42)^2
  },
  custom_objective_derivative = function(mu, sigma, ybest, d_mu, ...){
    (mu - 42) * d_mu
  }
)

## ## ## ## How to Use Formulas in lgspline ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
## Demonstrates splines with multiple mixed predictors and interactions

## Generate data
n <- 2500
x <- rnorm(n)
y <- rnorm(n)
z <- sin(x)*mean(abs(y))/2

## Categorical predictors
cat1 <- rbinom(n, 1, 0.5)
cat2 <- rbinom(n, 1, 0.5)
cat3 <- rbinom(n, 1, 0.5)

## Response with mix of effects
response <- y + z + 0.1*(2*cat1 - 1)

## Continuous predictors re-named
continuous1 <- x
continuous2 <- z

## Combine data
dat <- data.frame(
  response = response,
  continuous1 = continuous1,
  continuous2 = continuous2,
  cat1 = cat1,
  cat2 = cat2,
  cat3 = cat3
)

## Example 1: Basic Model with Default Terms, No Intercept
# standardize_response = FALSE often needed when constraining intercepts to 0
fit1 <- lgspline(
  formula = response ~ 0 + spl(continuous1, continuous2) +
    cat1*cat2*continuous1 + cat3,
  K = 2,
  standardize_response = FALSE,
  data = dat
)
## Examine coefficients included
rownames(fit1$B$partition1)
## Verify intercept term is near 0 up to some numeric tolerance
abs(fit1$B[[1]][1]) < 1e-8

## Example 2: Similar Model with Intercept, Other Terms Excluded
fit2 <- lgspline(
  formula = response ~ spl(continuous1, continuous2) +
    cat1*cat2*continuous1 + cat3,
  K = 1,
  standardize_response = FALSE,
  include_cubic_terms = FALSE,
  exclude_these_expansions = c( # Not all need to actually be present
    '_batman_x_robin_',
    '_3_x_4_', # no cat1 x cat2 interaction, coded using column indices
    'continuous1xcontinuous2', # no continuous1 x continuous2 interaction
    'thejoker'
  ),
  data = dat
)
## Examine coefficients included
rownames(Reduce('cbind',coef(fit2)))
# Intercept will probably be present and non-0 now
abs(fit2$B[[1]][1]) < 1e-8

## ## ## ## Compare Inference to survreg for Weibull AFT Model Validation ##
# Only linear predictors, no knots, no penalties, using Weibull AFT Model
# The goal here is to ensure that for the special case of no spline effects
# and no knots, this implementation will be consistent with other model
# implementations.
# Also note, that when using models (like Weibull AFT) where dispersion is
# being estimated and is required for estimating beta coefficients,
# we use a schur complement correction function to adjust (or "correct") our
# variance-covariance matrix for both estimation and inference to account for
# uncertainty in estimating the dispersion.
# Typically the schur_correction_function would return a negative-definite
# matrix, as its output is elementwise added to the information matrix prior
# to inversion.
if (requireNamespace("survival", quietly = TRUE)) {
  data("pbc", package = "survival")
  df <- data.frame(na.omit(
    pbc[, c("time", "trt", "stage", "hepato", "bili", "age", "status")]
  ))

  ## Weibull AFT using lgspline, showing how some custom options can be used to
  # fit more complicated models
  model_fit <- lgspline(time ~ trt + stage + hepato + bili + age,
                        df,
                        family = weibull_family(),
                        need_dispersion_for_estimation = TRUE,
                        dispersion_function = weibull_dispersion_function,
                        glm_weight_function = weibull_glm_weight_function,
                        schur_correction_function = weibull_schur_correction,
                        unconstrained_fit_fxn = unconstrained_fit_weibull,
                        opt = FALSE,
                        wiggle_penalty = 0,
                        flat_ridge_penalty = 0,
                        K = 0,
                        status = df$status != 0)
  print(summary(model_fit))

  ## Survreg results match closely on estimates and inference for coefficients
  survreg_fit <- survival::survreg(
    survival::Surv(time, status != 0) ~ trt + stage + hepato + bili + age,
    df
  )
  print(summary(survreg_fit))

  ## sigmasq_tilde = scale^2 of survreg
  print(c(sqrt(model_fit$sigmasq_tilde), survreg_fit$scale))
}

## ## ## ## Modelling Correlation Structures ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
## Setup
n_blocks <- 200 # Number of correlation_ids (subjects)
block_size <- 5 # Size of each correlation_ids (number of repeated measures per subj.)
N <- n_blocks * block_size # total sample size (balanced here)
rho_true <- 0.25  # True correlation

## Generate predictors and mean structure
t <- seq(-9, 9, length.out = N)
true_mean <- sin(t)

## Create block compound symmetric errors = I(1-p) + Jp
errors <- Reduce('rbind',
                 lapply(1:n_blocks,
                        function(i){
                          sigma <- diag(block_size) + rho_true *
                            (matrix(1, block_size, block_size) -
                               diag(block_size))
                          matsqrt(sigma) \%*\% rnorm(block_size)
                        }))

## Generate response with correlated errors
y <- true_mean + errors * 0.5

## Fit model with correlation structure
# include_warnings = FALSE is a good idea here, since many proposed
# correlations will not work
model_fit <- lgspline(t,
                      y,
                      K = 4,
                      correlation_id = rep(1:n_blocks, each = block_size),
                      correlation_structure = 'exchangeable',
                      include_warnings = FALSE
)

## Assess overall fit
plot(t, y, main = 'Sinosudial Fit Under Correlation Structure')
plot(model_fit, add = TRUE, show_formulas = TRUE, custom_predictor_lab = 't')

## Compare estimated vs true correlation
# Built-in exchangeable uses rho = exp(-exp(par)), so par in (-Inf, Inf)
# maps to rho in (0, 1). Only positive correlation is supported.
rho_est <- exp(-exp(model_fit$VhalfInv_params_estimates))
print(c("True correlation:" = rho_true,
        "Estimated correlation:" = rho_est))

## Quantify uncertainty in correlation estimate with 95\% confidence interval
#  CI is constructed on the working scale and back-transformed
ci_transformed <- confint(model_fit)['Correlation parameter 1',]
ci_natural <- sort(exp(-exp(ci_transformed)))
print("95\% CI for correlation:")
print(ci_natural)

## Also check SD (should be close to 0.5)
print(sqrt(model_fit$sigmasq_tilde))

## Toeplitz Simulation Setup, with demonstration of custom functions
# and boilerplate. Toep is not implemented by default, because it makes
# strong assumptions on the study design and missingness that are rarely met,
# with non-obvious workarounds.
# If a GLM was to-be-fit, you would also submit a function "Vhalf_fxn" analogous
# to VhalfInv_fxn with same argument (par) and an output of an N x N matrix
# that yields the inverse of VhalfInv_fxn output.
n_blocks <- 250   # Number of correlation_ids
block_size <- 8   # Observations per correlation_id
N <- n_blocks * block_size # total sample size
sigma_true <- 2   # Marginal standard deviation

## True Toeplitz components
# This example uses a convex combination of two geometric lag kernels:
# corr(h) = mix * rho_fast^h + (1 - mix) * rho_slow^h
# which is Toeplitz and positive definite for mix in (0, 1) and
# rho_fast, rho_slow in (0, 1).
rho_fast_true <- 0.25
rho_slow_true <- 0.75
mix_true <- 0.40

## Create time and correlation_id variables
time_var <- rep(1:block_size, n_blocks)
correlation_id_var <- rep(1:n_blocks, each = block_size)

## Create nonlinear predictor-response relationship
# Not sinusoidal and not polynomial.
t_base <- seq(-2, 2, length.out = block_size)
t <- rep(t_base, n_blocks) + rnorm(N, sd = 0.10)
f_true <- function(t) {
  1.4 + 0.9 * atan(1.8 * t) + 0.8 * exp(-1.2 * (t - 0.4)^2)
}

## Generate mean structure
mu_true <- f_true(t)

## Toeplitz correlation helper
corr_from_components <- function(rho_fast, rho_slow, mix) {
  corr <- matrix(0, block_size, block_size)
  for(i in 1:block_size) {
    for(j in 1:block_size) {
      lag <- abs(i - j)
      if(lag == 0) {
        corr[i, j] <- 1
      } else {
        corr[i, j] <- mix * rho_fast^lag + (1 - mix) * rho_slow^lag
      }
    }
  }
  corr
}

## Toeplitz correlation function
# Custom functions can use any parameterization. Here we map:
#   par[1] -> rho_fast = exp(-exp(par[1]))
#   par[2] -> rho_slow = exp(-exp(par[2]))
#   par[3] -> mix      = plogis(par[3])
# so the parameter space is unconstrained, while the resulting Toeplitz
# correlation matrix remains valid.
corr_from_par <- function(par) {
  rho_fast <- exp(-exp(par[1]))
  rho_slow <- exp(-exp(par[2]))
  mix <- plogis(par[3])
  corr_from_components(rho_fast, rho_slow, mix)
}

## Create block Toeplitz errors from the same family we will fit
corr_true <- corr_from_components(rho_fast_true, rho_slow_true, mix_true)
errors <- Reduce('c',
                 lapply(1:n_blocks, function(i) {
                   c(matsqrt(corr_true) \%*\% rnorm(block_size))
                 }))

## Generate response with correlated errors and nonlinear covariate effect
y <- mu_true + sigma_true * errors

VhalfInv_fxn <- function(par) {
  corr <- corr_from_par(par)
  kronecker(diag(n_blocks), matinvsqrt(corr))
}

Vhalf_fxn <- function(par) {
  corr <- corr_from_par(par)
  kronecker(diag(n_blocks), matsqrt(corr))
}

## Determinant function (for efficiency)
# This avoids taking determinant of N by N matrix
VhalfInv_logdet <- function(par) {
  corr <- corr_from_par(par)
  log_det_invsqrt_corr <- -0.5 * determinant(corr, logarithm = TRUE)$modulus[1]
  n_blocks * log_det_invsqrt_corr
}

## GLM weights for REML gradient helper
# For Gaussian identity, these are all 1.
glm_weight_function <- function(mu, y, order_indices, family,
                                dispersion, observation_weights, ...) {
  rep(1, length(mu))
}

## REML gradient function
# The helper reml_grad_from_dV computes the three REML terms once dV / dpar
# is supplied. For this parameterization, dV / dpar has closed form.
REML_grad <- function(par, model_fit, ...) {
  rho_fast <- exp(-exp(par[1]))
  rho_slow <- exp(-exp(par[2]))
  mix <- plogis(par[3])

  dV1_block <- matrix(0, block_size, block_size)
  dV2_block <- matrix(0, block_size, block_size)
  dV3_block <- matrix(0, block_size, block_size)

  for(i in 1:block_size) {
    for(j in 1:block_size) {
      lag <- abs(i - j)
      if(lag > 0) {
        ## d/dpar[1] through rho_fast = exp(-exp(par[1]))
        dV1_block[i, j] <- -mix * lag * exp(par[1]) * rho_fast^lag
        ## d/dpar[2] through rho_slow = exp(-exp(par[2]))
        dV2_block[i, j] <- -(1 - mix) * lag * exp(par[2]) * rho_slow^lag
        ## d/dpar[3] through mix = plogis(par[3])
        dV3_block[i, j] <- mix * (1 - mix) * (rho_fast^lag - rho_slow^lag)
      }
    }
  }

  dV1 <- kronecker(diag(n_blocks), dV1_block)
  dV2 <- kronecker(diag(n_blocks), dV2_block)
  dV3 <- kronecker(diag(n_blocks), dV3_block)

  gradient <- numeric(3)
  gradient[1] <- lgspline::reml_grad_from_dV(dV1, model_fit,
                                   glm_weight_function, ...)
  gradient[2] <- reml_grad_from_dV(dV2, model_fit,
                                   glm_weight_function, ...)
  gradient[3] <- reml_grad_from_dV(dV3, model_fit,
                                   glm_weight_function, ...)
  gradient
}

## Visualization
plot(t, y, col = correlation_id_var,
     main = 'Simulated Data with Toeplitz Correlation')

## Fit model with custom Toeplitz
model_fit <- lgspline(
  response = y,
  predictors = t,
  K = 4,
  standardize_response = FALSE,
  VhalfInv_fxn = VhalfInv_fxn,
  Vhalf_fxn = Vhalf_fxn,
  VhalfInv_logdet = VhalfInv_logdet,
  REML_grad = REML_grad,
  VhalfInv_par_init = c(0, -1, 0),
  include_warnings = FALSE
)

## Print comparison of true and estimated correlations
lag_values <- 1:(block_size - 1)
corr_true_by_lag <- sapply(lag_values, function(h) {
  mix_true * rho_fast_true^h + (1 - mix_true) * rho_slow_true^h
})
rho_fast_est <- exp(-exp(model_fit$VhalfInv_params_estimates[1]))
rho_slow_est <- exp(-exp(model_fit$VhalfInv_params_estimates[2]))
mix_est <- plogis(model_fit$VhalfInv_params_estimates[3])
corr_est_by_lag <- sapply(lag_values, function(h) {
  mix_est * rho_fast_est^h + (1 - mix_est) * rho_slow_est^h
})
cat('Toeplitz Correlation Estimates by Lag:\n')
print(data.frame(
  Lag = lag_values,
  True.Correlation = round(corr_true_by_lag, 4),
  Estimated.Correlation = round(corr_est_by_lag, 4)
))

## Should be ~ 2
print(sqrt(model_fit$sigmasq_tilde))

## ## ## ## Parallelism ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
if (requireNamespace("parallel", quietly = TRUE)) {
  ## Data generating function
  a <- runif(500000, -9, 9)
  b <- runif(500000, -9, 9)
  c <- rnorm(500000)
  d <- rpois(500000, 1)
  y <- sin(a) + cos(b) - 0.2*sqrt(a^2 + b^2) +
    abs(a) + b +
    0.5*(a^2 + b^2) +
    (1/6)*(a^3 + b^3) +
    a*b*c -
    c +
    d +
    rnorm(500000, 0, 5)

  ## Set up cores
  cl <- parallel::makeCluster(1)
  on.exit(try(parallel::stopCluster(cl), silent = TRUE), add = TRUE)

  ## This example shows some options for what operations can be parallelized
  # By default, only parallel_eigen and parallel_unconstrained are TRUE
  # parallel_unconstrained is only for GLMs, for identity link Gaussian
  # response, use parallel_matmult=TRUE to ensure parallel fitting across
  # partitions.
  # G, G^{-1/2}, and G^{1/2} are computed in parallel across each of the
  # K+1 partitions.
  # However, parallel_unconstrained only affects GLMs without corr. components
  # - it does not affect fitting here
  system.time({
    parfit <- lgspline(y ~ spl(a, b) + a*b*c + d,
                       data = data.frame(y = y,
                                         a = a,
                                         b = b,
                                         c = c,
                                         d = d),
                       cl = cl,
                       K = 1,
                       parallel_eigen = TRUE,
                       parallel_unconstrained = TRUE,
                       parallel_aga = FALSE,
                       parallel_find_neighbors = FALSE,
                       parallel_trace = FALSE,
                       parallel_matmult = TRUE,
                       parallel_make_constraint = FALSE,
                       parallel_penalty = FALSE)
  })
  print(summary(parfit))
}
}


}
\seealso{
\itemize{
  \item \code{\link{lgspline.fit}} for the low-level fitting interface
  \item \code{\link{logLik.lgspline}} for log-likelihood extraction
  \item \code{\link{confint.lgspline}} for confidence interval extraction
  \item \code{\link{leave_one_out}} for leave-one-out cross-validated predictions
  \item \code{\link{blockfit_solve}} for the standalone backfitting solver
  \item \code{\link[quadprog]{solve.QP}} for quadratic programming optimization
  \item \code{\link[plotly]{plot_ly}} for interactive plotting
  \item \code{\link[stats]{kmeans}} for k-means clustering
  \item \code{\link[stats]{optim}} for general purpose optimization routines
}
}
