                 Changes in version 2.0-2 (2026-10-14)                  

New solver

  - strata.data(..., method = "exact") runs Dynamic Programming on the
    data themselves. It returns the partition with the smallest
    empirical Neyman cost over all partitions into h contiguous strata
    with at least two units each, never splitting tied values. It is
    exact for data with up to 20,000 distinct cut points (about 15 to 20
    seconds for 14,000 units at h = 6); beyond that it searches 20,000
    cut points spread evenly in rank, which can be changed with
    options(stratifyR.exact_max_cand = ). The result records this in
    exact_full. The "dp" solver remains the default: it is globally
    optimal on a fine grid for the fitted distribution.

Bug fixes (all solvers)

  - The DP solver rounded its boundaries to two decimals, and the top
    boundary could round below the largest value, which then fell
    outside every stratum (for example 4,999 of 5,000 units counted).
    Boundaries are now kept at full precision and the top boundary is
    always the data maximum. Reported Neyman costs from 2.0-1 could be
    up to about 1 percent too low because of this.
  - method = "exact" places each boundary exactly on a data value. The
    allocation step rebuilt it as (b / maxval) * maxval, which on some
    platforms (for example Apple silicon) can round one bit below b and
    move the boundary unit into the next stratum, so the reported cost
    could be a little above the true optimum (0.25% for a Cauchy sample
    at h = 3). The allocation step now uses a cut point halfway to the
    next data value.
  - data.alloc() treated each stratum as closed at both ends, so a value
    equal to a boundary was counted in two strata. Every unit is now
    assigned to exactly one stratum, (b[h-1], b[h]].
  - WhShTot is now reported to six decimals instead of three.
  - Stratum sample sizes are now rounded with the largest-remainder
    rule, so they always add up to n and never exceed N_h. Rounding each
    n_h on its own could give a total of n + 1 or n - 1 (for example 63
    + 103 + 135 = 301 for the anaemia Iron data at h = 3, n = 300).
  - Every solver now returns h boundaries in OSB, the last being the top
    of the range.

Bug fixes (continuous solvers)

  - method = "cobyla" and method = "global" no longer force every
    stratum to be at least (range)/(3h) wide. That rule excluded the
    narrow strata that skewed and heavy-tailed populations need, so on
    Pareto, log-normal and Cauchy data the two solvers returned designs
    with up to several times the optimal Neyman cost. Boundaries now
    only need to be range/1000 apart, as in the problem formulation.
  - The degeneracy check no longer treats a narrow stratum as a failure
    and no longer silently switches to the DP solver. A solution is
    rejected only if a stratum holds fewer than two units.
  - method = "global" no longer accepts a result whose objective is the
    infeasibility penalty (a design with empty strata). If the global
    phase finds no feasible point, it falls back to multi-start COBYLA.
  - The random restarts no longer change the user's random number
    stream.
  - method = "global" no longer reports converged = FALSE when the
    BOBYQA refinement confirms the DIRECT-L point without improving it.

Shiny app

  - The strata-count (H) selector and the "Generate R code" button no
    longer pass an N argument to strata.data(), which has none.
    Previously this made the H curve drop points and the generated code
    fail when a population size was set.
  - Solver labels now describe GLOBAL as DIRECT-L followed by local
    refinement (the refinement step uses BOBYQA).
  - New stratifyR hex logo in the app header (and as the browser tab
    icon), and the version badge now reads the installed package version
    instead of a fixed "2.0-1".
  - New EXACT solver option for data sources. With an assumed
    distribution the app uses DP, which is exact for that distribution.
  - Help texts no longer describe COBYLA as unreliable on heavy tails;
    that was caused by the minimum-width rule fixed above.

Vignette

  - Section 7 is rewritten for the four solvers (DP, COBYLA, GLOBAL and
    EXACT), with a guide to choosing one and worked examples that
    include method = "exact".
  - New Section 8, "Working with the Results": reading the output,
    choosing the number of strata, compare_designs(), cost-aware
    allocation, plots and large frames, with a worked example on
    household expenditure.
  - The method comparison table (now Section 10) is recomputed
    with 2.0-2 and shows each solver's cost against the exact optimum.
  - Removed a development-only block that sourced the R files from the
    source tree, and updated the package details at the top.

Other

  - Distribution fitting no longer prints optimiser errors and warnings
    to the console for families that fail to fit; those families are
    skipped quietly, as before.
  - Added ORCID for the maintainer, and URL and BugReports fields.
  - DESCRIPTION now describes the four solvers correctly (GLOBAL refines
    with BOBYQA, not COBYLA).
  - fit_distribution() is documented as internal; it was marked for
    export but never exported.
  - Comments in cobyla.optim.R updated to match the 2.0-2 behaviour.
  - Help page for strata.data() corrected: "dp" is the default, and the
    "exact" method is documented.

                 Changes in version 2.0-1 (2026-09-10)                  

New features

  - strata.data() and strata.distr() gain a method argument supporting
    three optimisation strategies:
      - "dp" (default) — the original exhaustive Dynamic Programming
        grid search, guaranteed globally optimal on the discrete grid.
      - "cobyla" — multi-start gradient-free local optimiser (COBYLA)
        via nloptr; faster than DP for large h, suitable for exploratory
        work.
      - "global" — two-phase global optimiser combining DIRECT-L
        (deterministic space-filling search) followed by COBYLA local
        refinement; closest to DP accuracy among the two new methods.
  - New arguments n_starts, max_iter, tol, and verbose in both functions
    to control COBYLA/GLOBAL behaviour.
  - DESCRIPTION updated: nloptr added to Imports.
  - New compare_designs() function and print method. For the same total
    sample size it compares simple random sampling, proportional
    allocation and Neyman allocation, with design effects, the
    equivalent SRS sample size and the percentage cost saving.
  - New plot() method for "strata" objects with three types: "2d"
    (histogram or density with boundaries and the fitted density
    formula), "3d" and "interactive" (both need plotly).
  - New stratifyRApp() launches a self-contained shiny application:
    upload a CSV or Excel file or pick a built-in dataset, compute OSB
    and OSS with any solver, choose the number of strata, compare
    designs, explore the boundaries, download results and generate the
    equivalent R code.
  - The COBYLA and GLOBAL solvers support the same cost-aware objective
    as DP (cost = TRUE with per-stratum costs ch).
  - Solution-quality diagnostics for the COBYLA and GLOBAL solvers: a
    Cauchy-Schwarz optimality gap, KKT first-order residuals and a
    convergence flag, shown by summary().

Bug fixes

  - Weibull and Gamma recurrences (critical): distr.root() and
    data.root() were using the unregularized upper incomplete gamma
    function UGamma(s, z) (via zipfR) where the regularized form Q(s, z)
    was required by the mathematical formulation. Fixed by replacing all
    calls with pgamma(z, shape = s, lower.tail = FALSE), which correctly
    implements Q(s, z) = Γ(s, z) / Γ(s) as defined in Abramowitz &
    Stegun (1972), Chapter 6, equation 6.5.2. Results for Weibull and
    Gamma populations are now consistent with the published theory.
  - Triangular fitting in .refit_real_scale(): simultaneous MLE of all
    three triangular parameters (min, max, mode) produced degenerate
    estimates (min ≈ max ≈ mode). Fixed by fixing min and max to sample
    bounds and estimating only the mode via MLE, matching the approach
    already used in get.dist().
  - get.dist(): triangular distribution now only selected over a tail
    model (Gamma, Weibull, Log-normal, Exponential) when its AIC
    advantage exceeds 10 units, preventing spurious triangular wins on
    mildly right-skewed data.

Vignette

  - New Section 6: Alternative Optimisation Methods: COBYLA and GLOBAL —
    describes the motivation, algorithm, advantages, and limitations of
    each method, with a worked comparison example using the Anaemia/Iron
    data.
  - New Section 8: Summary of Method Comparison Across All Supported
    Distributions — presents a table of DP vs COBYLA vs GLOBAL results
    across all ten distributions with explanation of observed patterns.
  - Corrected description of Q(r, y) (regularized) vs Γ(r, y)
    (unregularized) in the Weibull and Gamma sections, with explicit R
    implementation note.
  - Fixed incorrect Normal distribution example (wrong mean/sd
    parameters).
  - Updated triangular parameter estimation example to use the fixed
    fix.arg approach.
  - Minor fixes: removed duplicate NeedsCompilation field, removed stray
    \url{} in Introduction.

Backward compatibility

  - All strata.data() / strata.distr() call signatures from v1.0-5 work
    without modification (method defaults to "dp").
  - The "strata" S3 class and all slot names are unchanged.

                 Changes in version 1.0-5 (2026-06-23)                  

Initial CRAN-submission-ready version. Implemented DP solver, 10-family
distribution fitting via AIC, Neyman allocation with cost support, and
coloured summary.strata() output.
