*! _trop_set_grid: configure regularization parameter grids

/*
    _trop_set_grid

    Construct candidate grids for the regularization parameter triplet
    lambda = (lambda_time, lambda_unit, lambda_nn) used in leave-one-out
    cross-validation (LOOCV).

    The LOOCV criterion Q(lambda) is minimized over a Cartesian product
    of candidate values for each component:

      lambda_time   exponential decay rate for time weights
                    theta_s = exp(-lambda_time * dist_time(s,t))

      lambda_unit   exponential decay rate for unit weights
                    omega_j = exp(-lambda_unit * dist_unit(j,i))

      lambda_nn     nuclear-norm penalty on the low-rank component L

    Three preset grid resolutions are available:

      default    6 x  6 x  5 =   180 combinations
      fine       7 x  7 x  7 =   343 combinations
      extended  14 x 16 x 19 = 4,256 combinations

    The `default` λ_nn grid (0, 0.01, 0.1, 1, 10) is a five-point log-
    decade ladder covering the empirically relevant range for paper
    Eq. 2 without evaluating the DID/TWFE corner (λ_nn = +∞), keeping
    LOOCV cost predictable.

    `fine` adds half-decade λ_nn points (0.0316 and 0.316) in the critical
    0.01–1 band, filling the single-decade gap in the `default` λ_nn grid.
    On small panels (e.g. Basque, Germany) this substantially reduces the
    BLAS-dependent jitter in the selected λ_nn.

    `extended` additionally includes λ_nn = . (= +∞, the DID/TWFE
    corner per Eq. 2 remark).  Users who want LOOCV to consider the
    DID/TWFE special case should opt in via `grid_style(extended)` or
    pass a custom `lambda_nn_grid()`.

    User-supplied grids override the corresponding preset dimension.
    When any custom grid is active the style label is set to "custom".

    Returns via c_local:
      _lambda_time_grid   candidate values for lambda_time
      _lambda_unit_grid   candidate values for lambda_unit
      _lambda_nn_grid     candidate values for lambda_nn
      _grid_style         "default", "fine", "extended", or "custom"
      _n_time             number of lambda_time candidates
      _n_unit             number of lambda_unit candidates
      _n_nn               number of lambda_nn candidates
      _n_combinations     total grid size (product of the three)
      _n_per_cycle        evaluations per coordinate-descent cycle
*/


program define _trop_set_grid
    version 17.0
    syntax , [Grid_style(string) LAMbda_time_grid(string) ///
              LAMbda_unit_grid(string) LAMbda_nn_grid(string)]

    // --- detect user-supplied custom grids ---
    local has_custom_time = ("`lambda_time_grid'" != "")
    local has_custom_unit = ("`lambda_unit_grid'" != "")
    local has_custom_nn = ("`lambda_nn_grid'" != "")

    // --- resolve grid style ---
    if "`grid_style'" == "" {
        local grid_style "default"
    }

    if !inlist("`grid_style'", "default", "fine", "extended") {
        di as error "grid_style() must be 'default', 'fine', or 'extended'"
        di as error "  default:    180 combinations (6 x 6 x 5)"
        di as error "  fine:       343 combinations (7 x 7 x 7)"
        di as error "  extended: 4,256 combinations (14 x 16 x 19; includes DID/TWFE corner)"
        exit 198
    }

    // --- construct preset grids ---
    //
    // Grid values span from 0 (uniform weights / no penalty) to large
    // values (strong decay / heavy regularization).  lambda = 0 recovers
    // equal weighting; large lambda concentrates weight on nearby
    // units or periods and imposes stronger nuclear-norm shrinkage.

    // Per paper Eq 3 the weights θ, ω are exp(−λ · dist), defined for
    // λ ∈ [0, ∞); λ_time = λ_unit = 0 gives uniform weights.  λ_time =
    // λ_unit = ∞ is not a valid estimator configuration (it collapses
    // weight to the target period/unit only), so it is excluded from
    // every default and custom grid and rejected downstream.
    //
    // λ_nn = ∞ is the paper's DID/TWFE special case (Eq 2 remark: for
    // λ_nn = ∞, ω = θ = 1, we recover DID/TWFE).  It is included only in
    // the `extended` preset (via the Stata missing literal "."), so the
    // `default` preset keeps a five-point log-decade ladder for λ_nn
    // without the DID/TWFE corner; users who want LOOCV to evaluate it
    // should opt in explicitly via `extended` or a custom `lambda_nn_grid()`.
    if "`grid_style'" == "default" {
        // 6 x 6 x 5 = 180 combinations
        local default_time_grid "0 0.1 0.5 1 2 5"
        local default_unit_grid "0 0.1 0.5 1 2 5"
        local default_nn_grid "0 0.01 0.1 1 10"
    }
    else if "`grid_style'" == "fine" {
        // 7 x 7 x 7 = 343 combinations.  Adds 0.3 to λ_time/λ_unit and
        // 0.0316 / 0.316 to λ_nn (half-decade steps), filling gaps that
        // are critical on small panels where the LOOCV objective
        // Q(λ) surface exhibits non-convex plateaus.  DID/TWFE corner
        // (λ_nn = ∞) is still reserved for `extended`.
        local default_time_grid "0 0.1 0.3 0.5 1 2 5"
        local default_unit_grid "0 0.1 0.3 0.5 1 2 5"
        local default_nn_grid "0 0.01 0.0316 0.1 0.316 1 10"
    }
    else {
        // 14 x 16 x 19 = 4256 combinations; includes λ_nn = 10 and ∞.
        local default_time_grid "0 0.1 0.2 0.25 0.3 0.35 0.4 0.5 0.75 1 1.5 2 3 5"
        local default_unit_grid "0 0.1 0.2 0.25 0.3 0.35 0.4 0.5 0.75 1 1.2 1.5 1.6 2 3 5"
        local default_nn_grid "0 0.005 0.006 0.01 0.011 0.02 0.05 0.1 0.15 0.151 0.2 0.3 0.5 0.7 0.9 1 5 10 ."
    }

    // --- apply custom overrides ---
    // user-supplied grids take priority over preset values

    // Reject "." (Stata missing / inf) in lambda_time_grid and
    // lambda_unit_grid per paper Eq 3, which defines λ on [0, ∞) only.
    // λ_nn may legitimately be ∞ (DID/TWFE special case, Eq 2 remark).
    if `has_custom_time' {
        foreach _v of local lambda_time_grid {
            if "`_v'" == "." {
                di as error "lambda_time_grid() does not accept `.' (missing / inf)."
                di as error "  Paper Eq 3: θ_s = exp(−λ_time · |t − s|) is defined for"
                di as error "  λ_time ∈ [0, ∞); λ_time = 0 recovers uniform time weights."
                di as error "  λ_time = ∞ collapses all weight onto the target period,"
                di as error "  which is degenerate and not a supported configuration."
                exit 198
            }
        }
        local final_time_grid "`lambda_time_grid'"
    }
    else {
        local final_time_grid "`default_time_grid'"
    }

    if `has_custom_unit' {
        foreach _v of local lambda_unit_grid {
            if "`_v'" == "." {
                di as error "lambda_unit_grid() does not accept `.' (missing / inf)."
                di as error "  Paper Eq 3: ω_j = exp(−λ_unit · dist(j, i)) is defined for"
                di as error "  λ_unit ∈ [0, ∞); λ_unit = 0 recovers uniform unit weights."
                di as error "  λ_unit = ∞ collapses all weight onto the target unit,"
                di as error "  which is degenerate and not a supported configuration."
                exit 198
            }
        }
        local final_unit_grid "`lambda_unit_grid'"
    }
    else {
        local final_unit_grid "`default_unit_grid'"
    }

    if `has_custom_nn' {
        local final_nn_grid "`lambda_nn_grid'"
    }
    else {
        local final_nn_grid "`default_nn_grid'"
    }

    // --- count grid dimensions ---
    local n_time : word count `final_time_grid'
    local n_unit : word count `final_unit_grid'
    local n_nn : word count `final_nn_grid'
    local n_total = `n_time' * `n_unit' * `n_nn'

    // coordinate-descent cycling updates one parameter at a time while
    // holding the other two fixed, so each cycle requires
    // n_time + n_unit + n_nn LOOCV evaluations
    local n_per_cycle = `n_time' + `n_unit' + `n_nn'

    // --- relabel when any dimension is overridden ---
    local final_grid_style "`grid_style'"
    if `has_custom_time' | `has_custom_unit' | `has_custom_nn' {
        local final_grid_style "custom"
    }

    // --- return results to caller via c_local ---
    c_local _lambda_time_grid "`final_time_grid'"
    c_local _lambda_unit_grid "`final_unit_grid'"
    c_local _lambda_nn_grid "`final_nn_grid'"
    c_local _grid_style "`final_grid_style'"
    c_local _n_time `n_time'
    c_local _n_unit `n_unit'
    c_local _n_nn `n_nn'
    c_local _n_combinations `n_total'
    c_local _n_per_cycle `n_per_cycle'
end