rcompat-lbfgsb 0.1.6

Rust implementation of R-style stats::optim L-BFGS-B behavior
Documentation

rcompat-lbfgsb

rcompat-lbfgsb is a pure Rust crate that aims to reproduce the user-visible behavior of R's stats::optim(..., method = "L-BFGS-B").

The crate is a compatibility layer, not a general optimizer project. Its public API intentionally keeps R-like control names such as fnscale, parscale, ndeps, factr, pgtol, and lmm.

Clean-room implementation

rcompat-lbfgsb is not an official R project and does not copy or translate R source code. Compatibility is developed from public documentation and from parity fixtures generated by running R as an external reference during development. The crate does not require R at runtime.

Current status

This is an early implementation pass. The R-compatible scaling layer, validation rules, finite-difference gradients, public result model, and an in-tree bounded limited-memory quasi-Newton backend are implemented. The committed parity fixtures currently compare par, value, counts, convergence, and message against R-generated reference data for quadratic, Rosenbrock, bound-active, fixed-parameter, infinite-bound, scaling, finite-difference, iteration-limit, and compact objective-only DESeq2-derived negative-binomial GLM cases.

For multi-dimensional objective-only problems with fully finite bounds, such as DESeq2's [-30, 30] coefficient box, the native backend now uses the same main Cauchy/subspace path as supplied-gradient problems after building finite-difference gradients. Bound-active finite differences also preserve R's observed objective-call order, including forward-then-clamped-base calls for one-sided lower-bound coordinates. R's projected-gradient norm near finite bounds is also preserved by capping the component by the remaining distance to the relevant bound. A compact synthetic fixture guards R's line-search memory-refresh behavior in a finite-box objective-only nonconvex case. The committed hard-real DESeq2 guard covers 48 direct R objective-only optimizer replays and currently has exact optimizer-count parity for 36 of them, with worst parameter drift at 0.0034756018 and worst optimizer-count drift bounded at 8.

Known early limitations:

  • trace output is not byte-for-byte compatible with R
  • parity coverage is still small and should be expanded before claiming broad compatibility
  • the backend is implemented in this crate and still needs more adversarial fixture tuning
  • fixed parameters require a supplied gradient, matching R's observed behavior for no-gradient finite differences
  • DESeq2-derived fixture parameters can be less strict than likelihood and count parity in weakly identified directions

Example

use rcompat_lbfgsb::{optim_lbfgsb, Bounds, OptimControl};

let par = vec![0.0];
let bounds = Bounds::new(vec![-10.0], vec![10.0])?;
let control = OptimControl::default_for_dimension(1);

let result = optim_lbfgsb(
    par,
    bounds,
    |p| (p[0] - 2.0).powi(2),
    control,
)?;

assert!((result.par[0] - 2.0).abs() < 1e-5);
assert!(result.is_success());
# Ok::<(), rcompat_lbfgsb::OptimError>(())

Example with a supplied gradient

use rcompat_lbfgsb::{optim_lbfgsb_with_gradient, Bounds, OptimControl};

let result = optim_lbfgsb_with_gradient(
    vec![4.0],
    Bounds::new(vec![-10.0], vec![10.0])?,
    |p| (p[0] - 1.5).powi(2),
    |p| vec![2.0 * (p[0] - 1.5)],
    OptimControl::default_for_dimension(1),
)?;

assert!((result.par[0] - 1.5).abs() < 1e-5);
# Ok::<(), rcompat_lbfgsb::OptimError>(())

Semantics

  • parscale maps user parameters to internal parameters with internal_x[i] = user_par[i] / parscale[i].
  • fnscale maps user objective values to internal minimization values with internal_value = user_value / fnscale.
  • Negative fnscale supports R-style maximization.
  • ndeps is interpreted in the scaled parameter space for finite-difference gradients.
  • Returned OptimResult.par and OptimResult.value are in the user's original scale.

Runtime relationship to R

The crate does not call R, link against R, shell out to R, embed R, or depend on R at runtime. Optional fixture-generation scripts may be run manually by developers with R installed.