---
title: "Understanding Convergence in Demand Models"
author: "Brent Kaplan"
date: "`r Sys.Date()`"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Understanding Convergence in Demand Models}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(
    collapse = TRUE,
    comment = "#>",
    dev = "png",
    dpi = 144,
    fig.width = 7,
    fig.height = 5,
    warning = FALSE,
    message = FALSE
)

library(beezdemand)
```

## Introduction

If you've fitted demand models with `beezdemand`, you've likely encountered messages
like "convergence code 1", "false convergence", or a non-positive-definite `apVar`.
These can be alarming, especially if you're accustomed to linear models where
convergence is virtually guaranteed.
This vignette explains what these messages actually mean
and provides a systematic approach for evaluating whether your model results are
trustworthy.

Two myths persist in applied behavioral economics:

1. **"Convergence = good model."** A model can converge to a local minimum, a
   boundary solution, or nonsensical parameter estimates. Convergence status tells
   you the optimizer stopped happily, not that it stopped at the right place.

2. **"Non-convergence = useless results."** A model that hits an iteration limit
   or triggers a "false convergence" warning may have parameter estimates that are
   practically identical to those from a fully converged run. The optimizer's
   stopping rule fired before it was satisfied, but that doesn't mean the estimates
   are wrong.

This vignette covers convergence across all model tiers in `beezdemand`:

- **TMB models** (`fit_demand_tmb()`, `fit_demand_hurdle()`) report numeric codes
- **NLME models** (`fit_demand_mixed()`) report warning messages
- **Fixed-effect NLS models** (`fit_demand_fixed()`) either converge or fail outright

The diagnostic principles are the same across all tiers. By the end you'll have a
six-item checklist for evaluating any fitted demand model regardless of convergence
status.

## What Is Convergence?

### Optimization in Nonlinear Models

Unlike linear regression, which has a closed-form solution, nonlinear demand models
must be fitted iteratively. The optimizer starts from initial parameter values and
takes steps through parameter space, trying to minimize an objective function:

- **TMB models**: minimize negative log-likelihood (NLL)
- **NLME models**: minimize restricted/maximum likelihood (REML/ML)
- **NLS models**: minimize residual sum of squares (RSS)

At each iteration, the optimizer evaluates the objective function, computes
gradients (the direction of steepest descent), and takes a step. It stops when one
of several **stopping rules** fires:

1. **Gradient near zero**: the objective function is flat, suggesting a minimum
2. **Parameter change below tolerance**: successive iterations produce nearly
   identical estimates
3. **Iteration limit reached**: the budget of iterations is exhausted
4. **Step size collapses**: the optimizer can't make progress

The convergence status you see in model output tells you **which stopping rule
fired**. It does not tell you whether the solution is the global minimum, whether
the parameters are scientifically plausible, or whether the model fits the data
well.

### TMB Convergence Codes (nlminb)

TMB models fitted with `fit_demand_tmb()` and `fit_demand_hurdle()` use the
`nlminb` optimizer by default. It reports the following convergence codes:

| Code | What Triggered It | What It Means |
|:----:|-------------------|---------------|
| 0 | Relative improvement in objective < `rel.tol` | The optimizer is satisfied that it found a minimum. This is the "normal" exit. |
| 1 | Number of iterations >= `iter.max` | The optimizer ran out of its iteration budget before satisfying its convergence criterion. The estimates may be fine — it just wasn't given enough iterations to confirm. |
| 8 | Step size shrinks to zero but tolerance not met ("false convergence") | The optimizer cannot make further progress but hasn't confirmed it reached a minimum. Often occurs when the solution is near a parameter boundary or when the likelihood surface is very flat. |

**Code 0** is what you want but doesn't guarantee the solution is correct — the
optimizer may have converged to a local minimum or a boundary.

**Code 1** (iteration limit) is often benign. If the objective function was still
decreasing, increasing `iter_max` in `tmb_control` may resolve it. If the objective
had plateaued, the current estimates are likely at or very near the minimum.

**Code 8** (false convergence) sounds alarming but is common in well-specified
demand models. TMB provides exact gradients via automatic differentiation, so
code 8 is rarer than with finite-difference methods. When it does occur, it
typically means the solution is at a feasibility boundary (e.g., a random effect
variance near zero) rather than that the optimizer is stuck.

#### L-BFGS-B Convergence Codes

When using `optimizer = "L-BFGS-B"` in `tmb_control`, `fit_demand_tmb()` uses
`stats::optim()` with L-BFGS-B method. It reports different codes:

| Code | What Triggered It |
|:----:|-------------------|
| 0 | Successful convergence |
| 1 | Iteration limit reached (`maxit`) |
| 51 | Warning from L-BFGS-B (see `$message`) |
| 52 | Error in L-BFGS-B |

### NLME Convergence Warnings

NLME models fitted with `fit_demand_mixed()` use a fundamentally different
optimization architecture than TMB. Rather than a single optimizer minimizing a
single objective, NLME alternates between two procedures:

1. **EM algorithm**: estimates variance components (random effect variances and
   correlations)
2. **PNLS (penalized nonlinear least squares)**: estimates fixed effects and
   predicts random effects, conditional on the variance components

These two steps alternate until the overall likelihood converges. After
convergence, the **approximate variance-covariance matrix** (`apVar`) is computed
from the Hessian at the solution to provide standard errors for the variance
components.

NLME signals problems through R warnings rather than numeric codes. The package
captures these during fitting and stores them in the `fit_warnings` field of the
returned object. The following table summarizes the most common warnings:

| Warning Pattern | What It Means | Severity |
|----------------|---------------|----------|
| `"false convergence"` | Step size shrank below minimum before gradient converged | Moderate |
| `"step halving factor reduced below minimum"` | PNLS step-halving hit its floor | Moderate |
| `"maximum number of iterations reached"` | Hit `msMaxIter` or `maxIter` limit | Low |
| `"iteration limit reached"` | Same as above | Low |
| `"singular"` | Design or correlation matrix is rank-deficient | High |
| Non-PD `apVar` | Hessian not positive definite at solution | High |

#### Understanding Severity

**Low severity** (iteration limits): The model may just need more iterations. Try
increasing `msMaxIter` or `maxIter` in `nlme_control`. If the same estimates are
obtained with more iterations, the original solution was fine.

**Moderate severity** ("false convergence", "step halving"): These mean the optimizer
had difficulty making progress. The estimates may be fine — especially if the
parameters are plausible and the random effect variances are not near zero. These
warnings are analogous to TMB's code 8.

**High severity** ("singular", non-PD `apVar`): These suggest structural problems.
A singular correlation matrix means two random effects are almost perfectly
correlated, indicating overparameterization. A non-positive-definite `apVar` means
the Hessian at the solution is not well-behaved, so standard errors for variance
components are unreliable. However, the point estimates (fixed effects and random
effect predictions) may still be fine.

#### The apVar Issue

The `apVar` failure deserves special attention because it is common and often
misunderstood. You can check for it programmatically:

```{r apvar-check, eval = FALSE}
# After fitting an NLME model
fit <- fit_demand_mixed(data, y_var = "y_ll4", equation_form = "zben")

# apVar is a matrix when successful, a character string describing the error
# when it fails
is_apvar_ok <- is.matrix(fit$model$apVar)
```

When `apVar` fails:

- **Fixed-effect estimates** (`fixef(fit$model)`) are still valid — they don't
  depend on `apVar`
- **Random-effect predictions** (`ranef(fit$model)`) are still valid — they are
  empirical Bayes predictions conditional on the fixed effects
- **Variance component standard errors** are not available — you cannot construct
  confidence intervals for the random effect variances
- **`intervals(fit$model)`** will fail for the random effects portion

This is analogous to TMB's `sdreport` failure: point estimates are fine but
uncertainty quantification for variance parameters is compromised.

### NLS Convergence (Fixed-Effect Models)

`fit_demand_fixed()` handles convergence automatically through a multi-backend
fallback strategy. For each subject, the package tries:

1. `nls.multstart()` with random starting values
2. If that fails, `minpack.lm::nlsLM()` (Levenberg-Marquardt)
3. If that fails, `nlsr::wrapnlsr()`

Models either converge or fail outright; partial convergence doesn't occur. The
returned object tracks per-subject success/failure in the `Notes` column. Use
`check_demand_model()` to see the failure rate:

```{r fixed-convergence, eval = FALSE}
fit <- fit_demand_fixed(apt, equation = "hs")
diagnostics <- check_demand_model(fit)
print(diagnostics)
```

A high failure rate (>20%) suggests data quality problems — insufficient price
range, too many zero-consumption observations, or extreme outliers — rather than
modeling issues.

### The Key Insight

Convergence status reflects the optimizer's stopping rule, not the quality of the
solution. A non-converged fit with plausible parameters and valid standard errors
can be more trustworthy than a converged fit at a boundary or local minimum. This
principle applies equally to TMB numeric codes and NLME warning messages.

The next section provides a systematic checklist for evaluating fit quality
regardless of convergence status.

## Diagnostic Checklist

Use these six checks to evaluate any fitted demand model. Each check includes
parallel examples for TMB and NLME models.

### 1. Parameter Plausibility

The most important check is whether the estimated parameters make scientific sense.
For demand models:
- **Q0** (intensity) should be positive and in the range of observed consumption at
  low prices
- **alpha** (elasticity) should be positive and not extreme
- **k** (scaling constant) typically falls between 1 and 4 for purchase task data

#### TMB Models

```{r param-tmb, eval = FALSE}
fit_tmb <- fit_demand_tmb(data, y_var = "y", x_var = "x", id_var = "id")

# Population-level (fixed) parameters
coef(fit_tmb)

# Full summary with standard errors
summary(fit_tmb)

# Subject-level parameters (fixed + random effects)
subject_pars <- get_subject_pars(fit_tmb)
head(subject_pars)

# Check ranges
summary(subject_pars$Q0)
summary(subject_pars$alpha)
```

Look for:

- Negative Q0 or alpha values (biologically implausible)
- Extreme values (Q0 orders of magnitude larger than max observed consumption)
- Subject-level estimates that cluster at boundaries

#### NLME Models

```{r param-nlme, eval = FALSE}
fit_nlme <- fit_demand_mixed(data, y_var = "y_ll4", equation_form = "zben")

# Population-level fixed effects
fixef(fit_nlme$model)

# Subject-level deviations from population mean
ranef(fit_nlme$model)

# Subject-level parameters (fixed + random)
subject_pars <- get_subject_pars(fit_nlme)
head(subject_pars)
```

For NLME models, pay attention to whether the random effects are symmetric and
roughly normally distributed. Highly skewed or multimodal random effects suggest
model misspecification or influential subjects.

#### Hurdle Models

```{r param-hurdle, eval = FALSE}
fit_hurdle <- fit_demand_hurdle(data, y_var = "y", x_var = "x", id_var = "id")

# Population parameters
coef(fit_hurdle)

# Subject-level parameters
subject_pars <- get_subject_pars(fit_hurdle)
head(subject_pars)
```

### 2. Standard Errors and Variance Estimates

Finite, reasonable standard errors indicate that the likelihood surface is
well-behaved near the solution.

#### TMB Models

```{r se-tmb, eval = FALSE}
# Check if standard errors were successfully computed
fit_tmb$se_available

# Hessian positive-definiteness gate (since 0.3.0): TRUE means the inverse
# Hessian is well-conditioned, so SEs / p-values / Wald CIs are trustworthy.
# FALSE means they are not — and `fit_demand_tmb()` / `fit_demand_hurdle()`
# also emit a cli warning at fit time when verbose >= 1.
fit_tmb$hessian_pd

# View SEs in summary output (notes section flags pdHess = FALSE)
summary(fit_tmb)
```

Warning signs:

- `hessian_pd = FALSE`: standard errors and p-values may be unreliable
  (saddle point, near-singular Hessian, or boundary solution)
- `se_available = FALSE`: `TMB::sdreport()` failed entirely; SEs are missing
- Very large SEs (orders of magnitude larger than the estimates): flat likelihood
- `NA` or `NaN` SEs: numerical problems in the Hessian computation

#### NLME Models

```{r se-nlme, eval = FALSE}
# Check apVar status
is_apvar_ok <- is.matrix(fit_nlme$model$apVar)
cat("apVar OK:", is_apvar_ok, "\n")

# If apVar is OK, get confidence intervals on variance components
if (is_apvar_ok) {
  intervals(fit_nlme$model, which = "var-cov")
}

# Fixed effect standard errors are always available (from the PNLS step)
summary(fit_nlme$model)$tTable
```

Warning signs:

- `apVar` is a character string (not a matrix): variance component SEs unavailable
- Near-zero random effect variances: the random effect is not contributing to the
  model; consider removing it
- Very wide confidence intervals on variance components: model may be
  overparameterized

### 3. Gradient Norm (TMB-Specific)

For TMB models, you can check the gradient at the solution directly. A small
gradient confirms the optimizer stopped at (or very near) a stationary point.

```{r grad-tmb, eval = FALSE}
# Compute gradient at the solution
grad <- fit_tmb$tmb_obj$gr(fit_tmb$opt$par)
max_grad <- max(abs(grad))
cat("Maximum absolute gradient:", max_grad, "\n")

# Rule of thumb:
# < 0.001: excellent — optimizer found a clean minimum
# 0.001-0.01: good — likely near a minimum
# > 0.01: concerning — may not be at a minimum
```

NLME does not expose gradients directly, so this check is TMB-only. For NLME
models, rely on the other five checks.

### 4. Hessian / apVar Status

The Hessian matrix (second derivatives of the objective function) tells you about
the curvature at the solution. A positive-definite Hessian confirms the solution is
at a local minimum (not a saddle point or maximum).

#### TMB Models

```{r hess-tmb, eval = FALSE}
# Preferred (since 0.3.0): the dedicated `hessian_pd` accessor on the fit
# object. TRUE = positive definite; FALSE = not; NA = sdreport failed.
fit_tmb$hessian_pd

# Equivalent low-level path through the sdreport object:
if (!is.null(fit_tmb$sdr)) {
  isTRUE(fit_tmb$sdr$pdHess)
}
```

If `hessian_pd` is `FALSE`, the solution may be at a saddle point or on the
boundary of the feasible region. The fitting function emits a cli warning at
fit time when `verbose >= 1`, and `summary()` adds a corresponding note.
`tidy()` returns its data unchanged but tags the result with a
`hessian_warning` attribute for downstream pipelines:

```{r hess-tmb-tidy, eval = FALSE}
out <- tidy(fit_tmb)
attr(out, "hessian_warning")
```

When triggered, try refitting with different starting values, switching
optimizers (`tmb_control$optimizer = "L-BFGS-B"`), or warm-starting from the
flagged fit (`tmb_control$warm_start = fit_tmb$opt$par`). The same
`hessian_pd` field exists on `beezdemand_hurdle` objects.

#### NLME Models

```{r hess-nlme, eval = FALSE}
# Check apVar (NLME's version of the Hessian check)
cat("apVar OK:", is.matrix(fit_nlme$model$apVar), "\n")

# Check random effects correlation for near-singularity
vc <- nlme::VarCorr(fit_nlme$model)
print(vc)
# Look at the Corr column — values > |0.99| indicate near-singularity
```

For NLME models, also check the random effects correlation matrix. If two random
effects are correlated above |0.99|, they are nearly collinear — the model is
trying to estimate two separate sources of variation that the data cannot
distinguish. Consider switching to `pdDiag` covariance structure (which forces
zero correlation) or removing one of the random effects.

### 5. Comparison with Nested Converged Models

If a non-converged complex model beats a simpler converged model by a large AIC
margin, the parameters are in a good region of the likelihood surface. AIC and BIC
depend on the log-likelihood at the parameter values, not on convergence status.

#### TMB Models

```{r compare-tmb, eval = FALSE}
# Fit a simpler model (e.g., fewer random effects)
fit_simple <- fit_demand_tmb(data, y_var = "y", x_var = "x", id_var = "id",
                             random_effects = "q0")
fit_complex <- fit_demand_tmb(data, y_var = "y", x_var = "x", id_var = "id",
                              random_effects = c("q0", "alpha"))

# Compare
AIC(fit_simple, fit_complex)
```

#### NLME Models

```{r compare-nlme, eval = FALSE}
# Fit with random effect on Q0 only vs. Q0 + alpha
fit_q0 <- fit_demand_mixed(data, y_var = "y_ll4", equation_form = "zben",
                           random_effects = Q0 ~ 1)
fit_both <- fit_demand_mixed(data, y_var = "y_ll4", equation_form = "zben",
                             random_effects = Q0 + alpha ~ 1)

# Compare AIC
AIC(fit_q0$model, fit_both$model)

# Or use anova for a formal likelihood ratio test
anova(fit_q0$model, fit_both$model)
```

### 6. Stability Across Starting Values

If different starting values lead to the same (or very similar) parameter estimates,
you can be confident the solution is not a local minimum.

#### TMB Models

`fit_demand_tmb()` performs multi-start optimization automatically. You can verify
stability by checking whether different starting configurations yield the same
negative log-likelihood:

```{r stability-tmb, eval = FALSE}
# Fit with default multi-start
fit1 <- fit_demand_tmb(data, y_var = "y", x_var = "x", id_var = "id")

# Fit with warm start from the first fit
fit2 <- fit_demand_tmb(data, y_var = "y", x_var = "x", id_var = "id",
                       tmb_control = list(warm_start = fit1$opt$par))

# Compare NLL values
cat("Fit 1 NLL:", fit1$opt$objective, "\n")
cat("Fit 2 NLL:", fit2$opt$objective, "\n")
```

#### NLME Models

```{r stability-nlme, eval = FALSE}
# Fit with pooled NLS starting values (data-driven)
fit_pooled <- fit_demand_mixed(data, y_var = "y_ll4", equation_form = "zben",
                               start_values = "pooled_nls")

# Fit with different manual starting values
fit_manual <- fit_demand_mixed(data, y_var = "y_ll4", equation_form = "zben",
                               start_values = list(
                                 fixed = c(Q0 = 5, alpha = 0.001)
                               ))

# Compare fixed effects
fixef(fit_pooled$model)
fixef(fit_manual$model)
```

If both runs yield similar fixed-effect estimates (within a few percent), the
solution is stable.

## Decision Framework

### TMB Decision Table

Use this table after running the diagnostic checklist on a TMB model
(`fit_demand_tmb()` or `fit_demand_hurdle()`):

| Code | Gradient Small? | Hessian PD? | SEs Finite? | Params Plausible? | Verdict |
|:----:|:---:|:---:|:---:|:---:|---------|
| 0 | Yes | Yes | Yes | Yes | **Trust the results** |
| 0 | Yes | No | — | — | Saddle point: refit with different starts |
| 0 | Yes | Yes | Yes | No | Converged to wrong minimum: check data, try different starts |
| 1 or 8 | Yes | Yes | Yes | Yes | **Likely trustworthy** — run diagnostics, increase iterations to confirm |
| 1 or 8 | No | No | — | — | Simplify model (fewer random effects, fix k) |
| 1 or 8 | — | — | — | No | Estimates unreliable: simplify or check data quality |

### NLME Decision Table

Use this table after running the diagnostic checklist on an NLME model
(`fit_demand_mixed()`):

| Warning | apVar OK? | RE Corr < \|0.99\|? | RE Var > 0? | Params Plausible? | Verdict |
|---------|:---:|:---:|:---:|:---:|---------|
| None | Yes | Yes | Yes | Yes | **Trust the results** |
| "false convergence" | Yes | Yes | Yes | Yes | **Likely trustworthy** — consider increasing iterations |
| "false convergence" | No | — | — | — | SEs unreliable: consider simplifying RE structure |
| "max iterations" | — | — | — | Yes | Increase `msMaxIter`/`maxIter` and refit |
| "singular" | — | No | — | — | Overparameterized: reduce RE structure |
| Any | — | — | Near zero | — | Remove that random effect |
| Any | — | — | — | No | Estimates unreliable: check data, simplify model |

## Remedies by Model Tier

### NLME Remedies

NLME convergence issues typically fall into a few categories. The table below maps
symptoms to solutions:

| Problem | Symptom | Remedy |
|---------|---------|--------|
| Overparameterized RE | Near-singular correlation, RE variance near 0 | Remove random effect on alpha; keep only Q0 |
| Slow convergence | "maximum number of iterations reached" | Increase `msMaxIter` and `maxIter` |
| False convergence | "false convergence" warning | Loosen `pnlsTol`, increase `niterEM` |
| Non-PD apVar | `is.character(model$apVar)` | Switch to `pdDiag` covariance structure |
| Bad starting values | Immediate failure or wild estimates | Use pooled NLS starts or manually specify |
| LL4 transform issues | Extreme residuals, poor fit | Check lambda parameter; verify y_ll4 values |

#### Increasing Iterations

The simplest remedy for iteration-limit warnings. The `nlme_control` argument
accepts a named list of control parameters:

```{r nlme-iterations, eval = FALSE}
fit <- fit_demand_mixed(
  data,
  y_var = "y_ll4",
  equation_form = "zben",
  nlme_control = list(
    msMaxIter = 500,  # Max iterations for the outer optimization (default: 200)
    maxIter = 500,    # Max iterations for the PNLS step (default: 200)
    niterEM = 200     # Number of EM iterations before switching to Newton (default: 100)
  )
)
```

#### Loosening Tolerance for False Convergence

When the PNLS step reports "false convergence", it means the step size shrank below
the minimum tolerance before the gradient converged. Loosening the PNLS tolerance
allows the optimizer more room to maneuver:

```{r nlme-tolerance, eval = FALSE}
fit <- fit_demand_mixed(
  data,
  y_var = "y_ll4",
  equation_form = "zben",
  nlme_control = list(
    pnlsTol = 1e-2,     # Loosen PNLS tolerance (default: 1e-3)
    tolerance = 1e-5,    # Slightly loosen overall tolerance (default: 1e-6)
    niterEM = 150        # More EM iterations can help with difficult surfaces
  )
)
```

#### Switching Covariance Structure

If the random effects correlation is near |1.0| or `apVar` fails, switching to a
diagonal covariance structure (`pdDiag`) removes the correlation parameter entirely.
This reduces the number of parameters and often resolves convergence issues:
```{r nlme-covariance, eval = FALSE}
# Default uses pdDiag (diagonal); try pdSymm for unstructured if you have
# enough data, or stay with pdDiag if convergence is problematic
fit_diag <- fit_demand_mixed(
  data,
  y_var = "y_ll4",
  equation_form = "zben",
  covariance_structure = "pdDiag"  # No correlation between random effects
)

# Compare with unstructured (pdSymm) if convergence allows
fit_symm <- fit_demand_mixed(
  data,
  y_var = "y_ll4",
  equation_form = "zben",
  covariance_structure = "pdSymm"  # Estimate correlation between random effects
)
```

#### Simplifying Random Effects

If a random effect variance is near zero, the data does not support
subject-level variation in that parameter. Removing it simplifies the model and
often resolves convergence problems:

```{r nlme-simplify-re, eval = FALSE}
# Random effects on both Q0 and alpha (more complex)
fit_both <- fit_demand_mixed(
  data,
  y_var = "y_ll4",
  equation_form = "zben",
  random_effects = Q0 + alpha ~ 1
)

# Random effect on Q0 only (simpler, often sufficient)
fit_q0_only <- fit_demand_mixed(
  data,
  y_var = "y_ll4",
  equation_form = "zben",
  random_effects = Q0 ~ 1
)

# Compare fit
AIC(fit_both$model, fit_q0_only$model)
```

### TMB Remedies

TMB convergence issues have their own set of solutions:

| Problem | Symptom | Remedy |
|---------|---------|--------|
| Local minimum | Multiple starts give different NLL | Increase multi-start sets |
| Flat likelihood | Large SEs, code 1 | Simplify model (fewer RE, fix k) |
| Boundary solution | Parameter at bound, code 1 | Widen or narrow bounds |
| Iteration limit | Code 1 | Increase `iter_max` |
| False convergence | Code 8 | Try more iterations; try L-BFGS-B |
| Saddle point | Hessian not PD | Different starts; try L-BFGS-B optimizer |
| Zero RE variance | sigma near 0 | Remove that random effect |

#### Adjusting TMB Control

```{r tmb-control, eval = FALSE}
fit <- fit_demand_tmb(
  data,
  y_var = "y", x_var = "x", id_var = "id",
  tmb_control = list(
    iter_max = 2000,      # Increase iteration limit (default: 1000)
    eval_max = 4000,      # Increase function evaluation limit (default: 2000)
    rel_tol = 1e-12       # Tighten relative tolerance for more precision
  )
)
```

#### Switching Optimizer

If `nlminb` produces code 8 (false convergence), switching to L-BFGS-B sometimes
helps because it uses a different line search strategy:

```{r tmb-optimizer, eval = FALSE}
fit_lbfgsb <- fit_demand_tmb(
  data,
  y_var = "y", x_var = "x", id_var = "id",
  tmb_control = list(optimizer = "L-BFGS-B")
)
```

#### Using Warm Starts

If you have a partially converged fit, you can use it as a starting point for a
fresh optimization:

```{r tmb-warmstart, eval = FALSE}
# First fit (may not fully converge)
fit1 <- fit_demand_tmb(data, y_var = "y", x_var = "x", id_var = "id")

# Use as warm start for a second attempt
fit2 <- fit_demand_tmb(data, y_var = "y", x_var = "x", id_var = "id",
                       tmb_control = list(warm_start = fit1$opt$par))
```

#### Hurdle Model Remedies

Hurdle models have additional options for controlling complexity:

```{r hurdle-remedies, eval = FALSE}
# Reduce random effects: 2-RE instead of 3-RE
fit_2re <- fit_demand_hurdle(
  data,
  y_var = "y", x_var = "x", id_var = "id",
  random_effects = c("zeros", "q0")  # Drop alpha random effect
)

# Try a different Part II equation
fit_snd <- fit_demand_hurdle(
  data,
  y_var = "y", x_var = "x", id_var = "id",
  part2 = "simplified_exponential"  # Fewer parameters (no k)
)
```

### Fixed-Effect NLS Remedies

`fit_demand_fixed()` handles convergence automatically via multi-backend fallback.
When many subjects fail to converge, the issue is almost always data quality rather
than model specification:

- **Too few price points**: demand curves need at least 4-5 prices to fit reliably
- **No variation in consumption**: if consumption is constant across prices, the
  model has nothing to fit
- **Extreme outliers**: a single wildly atypical data point can prevent convergence
- **Too many zeros**: consider `fit_demand_hurdle()` instead, which handles zeros
  explicitly

```{r fixed-remedies, eval = FALSE}
# Check data quality first
systematic <- CheckUnsystematic(data, deltaq = 0.025, bounce = 0.1, reversals = 0)
table(systematic$TotalPass)

# Try a different equation if "hs" fails for many subjects
fit_koff <- fit_demand_fixed(data, equation = "koff")
```

## Using check_demand_model()

The `check_demand_model()` function automates the most important diagnostic checks
for all model tiers. It returns a `beezdemand_diagnostics` object that summarizes
convergence status, boundary conditions, residual patterns, and random effect
diagnostics.

### TMB Example

```{r check-tmb, eval = FALSE}
fit_hurdle <- fit_demand_hurdle(apt, y_var = "y", x_var = "x", id_var = "id")
diagnostics <- check_demand_model(fit_hurdle)
print(diagnostics)
```

The output shows:

- **Convergence**: status and any optimizer messages
- **Random Effects**: variance estimates and whether any are near zero
- **Residuals**: summary statistics and outlier count
- **Issues/Recommendations**: actionable suggestions

### NLME Example

```{r check-nlme, eval = FALSE}
fit_nlme <- fit_demand_mixed(data, y_var = "y_ll4", equation_form = "zben")
diagnostics <- check_demand_model(fit_nlme)
print(diagnostics)
```

For NLME models, the diagnostics additionally check for:

- Stored fit warnings (e.g., "false convergence")
- `apVar` status (Hessian positive-definiteness)
- Random effect correlation near-singularity (|corr| > 0.99)

### Live Example with Fixed-Effect Model

Here is a live example using the built-in `apt` dataset:

```{r check-fixed-live}
data(apt)
fit <- fit_demand_fixed(apt, equation = "hs")
diagnostics <- check_demand_model(fit)
print(diagnostics)
```

### Accessing Fit Warnings Directly

For NLME models, you can also inspect the captured warnings directly:

```{r fit-warnings, eval = FALSE}
# Warnings captured during NLME fitting
fit_nlme$fit_warnings

# These are also surfaced by glance()
glance_result <- broom::glance(fit_nlme)
glance_result$converged
```

## Summary

Key takeaways:

1. **Convergence codes and warnings are about stopping rules, not solution quality.**
   Code 0 or no warnings means the optimizer is satisfied — it doesn't guarantee a
   good solution. Non-convergence doesn't mean the estimates are wrong.

2. **Use the six-item diagnostic checklist.** Parameter plausibility, standard
   errors, gradient norm (TMB), Hessian/apVar status, model comparison, and
   starting value stability.

3. **NLME and TMB have different mechanics but the same diagnostic principles.**
   NLME uses EM + PNLS iterations and reports warnings; TMB uses direct likelihood
   optimization and reports numeric codes. Both can produce trustworthy results even
   when not fully converged.

4. **`check_demand_model()` automates the most important checks.** Use it on any
   fitted model to get a quick summary of potential issues.

5. **When in doubt, fit simpler and compare.** Reduce random effects, try diagonal
   covariance, or switch equation forms. If a simpler converged model produces
   similar estimates to a non-converged complex model, the estimates are likely
   trustworthy.

### Cross-References

- `vignette("hurdle-demand-models")` — Fitting TMB hurdle models
- `vignette("mixed-demand")` — Fitting NLME mixed-effects models
- `vignette("mixed-demand-advanced")` — Advanced NLME topics including control tuning
- `vignette("fixed-demand")` — Fitting fixed-effect NLS models
- `vignette("model-selection")` — Choosing between model tiers

## References

- Gay, D. M. (1990). Usage summary for selected optimization routines (Computing
  Science Technical Report No. 153). AT&T Bell Laboratories. (Describes the PORT
  routines underlying `nlminb`.)

- Kristensen, K., Nielsen, A., Berg, C. W., Skaug, H., & Bell, B. M. (2016). TMB:
  Automatic differentiation and Laplace approximation. *Journal of Statistical
  Software*, *70*(5), 1-21.

- Nocedal, J., & Wright, S. J. (2006). *Numerical Optimization* (2nd ed.).
  Springer.

- Pinheiro, J. C., & Bates, D. M. (2000). *Mixed-Effects Models in S and S-PLUS*.
  Springer. (The definitive reference for the `nlme` package's optimization
  architecture.)
