Skip to main content

molpack/gencan/
mod.rs

1//! GENCAN optimizer — faithful Rust port of `gencan.f` and `pgencan.f90`.
2//!
3//! Reference: Birgin & Martinez, Comp.Opt.Appl. 23:101-125, 2002.
4
5use molrs::types::F;
6pub mod cg;
7pub mod spg;
8
9use crate::constraints::EvalMode;
10use crate::numerics::{numeric_controls, positive_norm_floor};
11use crate::objective::Objective;
12
13/// Parameters for the GENCAN call (matches `easygencan` defaults from `pgencan.f90`).
14pub struct GencanParams {
15    pub epsgpsn: F,
16    pub maxit: usize,
17    pub maxfc: usize,
18    pub delmin: F,
19    pub iprint: i32,
20    pub ncomp: usize,
21}
22
23impl Default for GencanParams {
24    fn default() -> Self {
25        Self {
26            epsgpsn: 1.0e-6,
27            maxit: 20,
28            maxfc: 200,     // 10 * maxit
29            delmin: 1.0e-2, // Packmol easygencan default (gencan.f: delmin = 1.d-2)
30            iprint: 0,
31            ncomp: 50,
32        }
33    }
34}
35
36/// Result of a GENCAN run.
37pub struct GencanResult {
38    pub f: F,
39    pub gpsupn: F,
40    pub iter: usize,
41    pub fcnt: usize,
42    pub gcnt: usize,
43    pub cgcnt: usize,
44    /// 0=converged(eucl), 1=converged(sup), 2=noFprogress, 3=noGprogress,
45    /// 4=fSmall, 7=maxIter, 8=maxFeval, <0=error
46    pub inform: i32,
47}
48
49/// Reusable work buffers for repeated `pgencan` calls.
50pub struct GencanWorkspace {
51    g: Vec<F>,
52    ind: Vec<usize>,
53    d: Vec<F>,
54    s: Vec<F>,
55    y: Vec<F>,
56    cg_scratch: cg::CgScratch,
57    spg_scratch: spg::SpgScratch,
58    tnls_scratch: TnLsScratch,
59}
60
61impl GencanWorkspace {
62    pub fn new() -> Self {
63        Self {
64            g: Vec::new(),
65            ind: Vec::new(),
66            d: Vec::new(),
67            s: Vec::new(),
68            y: Vec::new(),
69            cg_scratch: cg::CgScratch::new(0),
70            spg_scratch: spg::SpgScratch::new(0),
71            tnls_scratch: TnLsScratch::new(0),
72        }
73    }
74
75    fn ensure_len(&mut self, n: usize) {
76        if self.g.len() != n {
77            self.g.resize(n, 0.0);
78        }
79        if self.d.len() != n {
80            self.d.resize(n, 0.0);
81        }
82        if self.s.len() != n {
83            self.s.resize(n, 0.0);
84        }
85        if self.y.len() != n {
86            self.y.resize(n, 0.0);
87        }
88        if self.ind.capacity() < n {
89            self.ind.reserve(n - self.ind.capacity());
90        }
91    }
92}
93
94impl Default for GencanWorkspace {
95    fn default() -> Self {
96        Self::new()
97    }
98}
99
100/// Entry point — mirrors `pgencan.f90` → `easygencan` → `gencan`.
101///
102/// The variable layout is:
103///   x[0..3N]   = COM positions (free molecules)
104///   x[3N..6N]  = Euler angles (free molecules)
105///
106/// Bounds: COM variables are unbounded; Euler angles may be bounded by
107/// `constrain_rotation` constraints (Packmol pgencan.f90).
108pub fn pgencan(
109    x: &mut [F],
110    obj: &mut dyn Objective,
111    params: &GencanParams,
112    precision: F,
113    workspace: &mut GencanWorkspace,
114) -> GencanResult {
115    let n = x.len();
116
117    let mut l = vec![0.0 as F; n];
118    let mut u = vec![0.0 as F; n];
119    obj.bounds(&mut l, &mut u);
120
121    gencan(x, &l, &u, obj, params, precision, workspace)
122}
123
124/// Main GENCAN loop.
125/// Port of `gencan.f` subroutine with Packmol-specific additions.
126#[allow(unused_assignments)]
127pub fn gencan(
128    x: &mut [F],
129    l: &[F],
130    u: &[F],
131    obj: &mut dyn Objective,
132    params: &GencanParams,
133    precision: F,
134    workspace: &mut GencanWorkspace,
135) -> GencanResult {
136    let n = x.len();
137    workspace.ensure_len(n);
138
139    // Constants (from easygencan parameters section)
140    const INFREL: F = 1.0e20;
141    const INFABS: F = F::MAX;
142    const BETA_LS: F = 0.5;
143    const GAMMA: F = 1.0e-4;
144    const THETA: F = 1.0e-6;
145    const SIGMA1: F = 0.1;
146    const SIGMA2: F = 0.9;
147    const MAXEXTRAP: usize = 100;
148    const MININTERP: usize = 4;
149    const NINT: F = 2.0;
150    const NEXT: F = 2.0;
151    const ETA: F = 0.9;
152    const LSPGMA: F = 1.0e10;
153    const LSPGMI: F = 1.0e-10;
154    const FMIN: F = 1.0e-5;
155    const EPSGPEN: F = 0.0;
156    let numeric = numeric_controls();
157
158    let cgepsi = 0.1 as F;
159    let cgepsf = 1.0e-5 as F;
160    let cggpnf = F::max(1.0e-4, params.epsgpsn);
161    let epsnqmp = 1.0e-4 as F;
162    let maxitnqmp = 5usize;
163    let epsnfp = 0.0 as F;
164    let maxitnfp = params.maxit;
165    let maxitngp = 1000usize;
166
167    // Project initial point
168    for i in 0..n {
169        x[i] = x[i].clamp(l[i], u[i]);
170    }
171
172    // Initial function value + gradient.
173    let mut fcnt = 0usize;
174    let g = &mut workspace.g;
175    g.fill(0.0);
176    let mut f = obj.evaluate(x, EvalMode::FAndGradient, Some(g)).f_total;
177
178    // Packmol behavior: check convergence before counting this first eval.
179    if converged(obj, precision) {
180        return GencanResult {
181            f,
182            gpsupn: 0.0,
183            iter: 0,
184            fcnt,
185            gcnt: 0,
186            cgcnt: 0,
187            inform: 0,
188        };
189    }
190    fcnt += 1;
191    let mut gcnt = 1usize;
192    let mut cgcnt = 0usize;
193
194    // Compute xnorm
195    let mut xnorm = x.iter().map(|xi| xi * xi).sum::<F>().sqrt();
196
197    let ind = &mut workspace.ind;
198    let cg_scratch = &mut workspace.cg_scratch;
199    let spg_scratch = &mut workspace.spg_scratch;
200    let tnls_scratch = &mut workspace.tnls_scratch;
201
202    // Compute projected gradient
203    let (mut gpsupn, mut gpeucn2, mut gieucn2, mut nind) =
204        projected_gradient_info(n, x, g.as_slice(), l, u, ind);
205
206    // CG epsilon scaling
207    let (acgeps, bcgeps) = gp_ieee_signal(gpsupn, cgepsf, cgepsi, cggpnf);
208
209    // Track initial projected gradient for kappa computation in cgmaxit
210    // (Packmol gp_ieee_signal2: cgscre=2, uses sup-norm)
211    let gpsupn0 = gpsupn;
212
213    let mut iter = 0usize;
214    let mut inform = 7i32; // default: max iterations
215
216    // Trust radius — computed fresh at the start of each TN iteration (see below).
217    // Fortran: iter==1 → max(delmin, 0.1*xnorm); iter>1 → max(delmin, 10*sqrt(sts)).
218    let mut delta = 0.0 as F; // placeholder; set before each cg_solve call
219
220    // BB spectral step
221    let mut sts = 0.0 as F;
222    let mut sty = 0.0 as F;
223    let ometa2 = (1.0 - ETA).powi(2);
224
225    // No-progress tracking
226    let mut fprev = INFABS;
227    let mut bestprog = 0.0 as F;
228    let mut itnfp = 0usize;
229    let mut lastgpns = vec![INFABS; maxitngp];
230
231    // Working vectors
232    let d = &mut workspace.d;
233    let s = &mut workspace.s;
234    let y = &mut workspace.y;
235
236    // Main loop
237    loop {
238        // Packmol behavior: recompute precision test with computef at each iteration.
239        if converged(obj, precision) {
240            break;
241        }
242
243        if gpeucn2 <= EPSGPEN * EPSGPEN {
244            inform = 0;
245            break;
246        }
247        // Check convergence: sup-norm of projected gradient
248        if gpsupn <= params.epsgpsn {
249            inform = 1;
250            break;
251        }
252
253        // No function progress
254        let currprog = fprev - f;
255        bestprog = bestprog.max(currprog);
256        if currprog <= epsnfp * bestprog {
257            itnfp += 1;
258            if itnfp >= maxitnfp {
259                inform = 2;
260                break;
261            }
262        } else {
263            itnfp = 0;
264        }
265
266        // No gradient progress
267        let gpnmax = lastgpns.iter().copied().fold(0.0 as F, F::max);
268        lastgpns[iter % maxitngp] = gpeucn2;
269        if gpeucn2 >= gpnmax {
270            inform = 3;
271            break;
272        }
273
274        if f <= FMIN {
275            inform = 4;
276            break;
277        }
278        if iter >= params.maxit {
279            inform = 7;
280            break;
281        }
282        if fcnt >= params.maxfc {
283            inform = 8;
284            break;
285        }
286
287        // New iteration
288        iter += 1;
289        fprev = f;
290
291        // Save x → s, g → y
292        s.copy_from_slice(x);
293        y.copy_from_slice(g.as_slice());
294
295        if gieucn2 <= ometa2 * gpeucn2 {
296            // SPG iteration: abandon current face
297            let lamspg = if iter == 1 || sty <= 0.0 {
298                F::max(1.0, xnorm) / gpeucn2.sqrt().max(positive_norm_floor())
299            } else {
300                sts / sty
301            };
302            let lamspg = lamspg.clamp(LSPGMI, LSPGMA);
303
304            let spg_res = spg::spgls(
305                n,
306                x,
307                g.as_slice(),
308                l,
309                u,
310                lamspg,
311                f,
312                NINT,
313                MININTERP,
314                FMIN,
315                params.maxfc,
316                fcnt,
317                GAMMA,
318                SIGMA1,
319                SIGMA2,
320                numeric.sterel,
321                numeric.steabs,
322                numeric.epsrel,
323                numeric.epsabs,
324                spg_scratch,
325                obj,
326            );
327            f = spg_res.f;
328            fcnt = spg_res.fcnt;
329            x.copy_from_slice(&spg_scratch.xtrial);
330
331            if spg_res.inform < 0 {
332                inform = spg_res.inform;
333                break;
334            }
335
336            obj.evaluate(x, EvalMode::GradientOnly, Some(g.as_mut_slice()));
337            gcnt += 1;
338        } else {
339            // TN iteration: compute Newton direction via CG
340
341            // Compute trust-region radius (Fortran gencan.f lines 2120-2128):
342            //   iter==1: delta = max(delmin, 0.1 * max(1, xnorm))
343            //   iter>1:  delta = max(delmin, 10 * sqrt(sts))
344            delta = if iter == 1 {
345                F::max(params.delmin, 0.1 * F::max(1.0, xnorm))
346            } else {
347                F::max(params.delmin, 10.0 * sts.sqrt())
348            };
349
350            let cgeps = compute_cgeps(gpsupn, acgeps, bcgeps, cgepsf, cgepsi);
351
352            // Packmol gp_ieee_signal2 formula (cgscre=2, nearlyq=false):
353            //   kappa = clamp(log10(gpsupn/gpsupn0) / log10(epsgpsn/gpsupn0), 0, 1)
354            //   cgmaxit = min(20, (1-kappa)*max(1, 10*log10(nind)) + kappa*nind)
355            let cgmaxit = {
356                let mut kappa = (gpsupn / gpsupn0).log10() / (params.epsgpsn / gpsupn0).log10();
357                kappa = F::max(0.0, F::min(1.0, kappa));
358
359                let nind_f = nind as F;
360                let base = (1.0 - kappa) * F::max(1.0, 10.0 * nind_f.log10()) + kappa * nind_f;
361                usize::min(20, base as usize)
362            };
363
364            let cg_res = cg::cg_solve(
365                nind,
366                ind,
367                n,
368                x,
369                g.as_slice(),
370                delta,
371                l,
372                u,
373                cgeps,
374                epsnqmp,
375                maxitnqmp,
376                cgmaxit,
377                false, // nearlyq = .false. in packmol easygencan defaults
378                1,     // trtype=1 (sup-norm)
379                THETA,
380                numeric.sterel,
381                numeric.steabs,
382                numeric.epsrel,
383                numeric.epsabs,
384                INFREL,
385                INFABS,
386                d,
387                cg_scratch,
388                obj,
389            );
390            cgcnt += cg_res.iter;
391
392            // Compute maximum feasible step along d (packmol gencan.f lines 2204-2225).
393            let mut amax = INFABS;
394            let mut rbdtype = 0i32;
395            let mut rbdind = if nind > 0 { ind[0] } else { 0 };
396
397            if cg_res.inform == 2 {
398                amax = 1.0;
399                rbdtype = cg_res.rbdtype;
400                rbdind = cg_res.rbdind.unwrap_or(rbdind);
401            } else {
402                for &ii in &ind[..nind] {
403                    if d[ii] > 0.0 {
404                        let amaxx = (u[ii] - x[ii]) / d[ii];
405                        if amaxx < amax {
406                            amax = amaxx;
407                            rbdind = ii;
408                            rbdtype = 2;
409                        }
410                    } else if d[ii] < 0.0 {
411                        let amaxx = (l[ii] - x[ii]) / d[ii];
412                        if amaxx < amax {
413                            amax = amaxx;
414                            rbdind = ii;
415                            rbdtype = 1;
416                        }
417                    }
418                }
419            }
420
421            // TN line search (full port of tnls behavior).
422            let ls_res = tn_linesearch(
423                nind,
424                ind,
425                n,
426                x,
427                g.as_slice(),
428                d,
429                l,
430                u,
431                f,
432                amax,
433                rbdtype,
434                rbdind,
435                NINT,
436                NEXT,
437                MININTERP,
438                MAXEXTRAP,
439                FMIN,
440                params.maxfc,
441                fcnt,
442                gcnt,
443                GAMMA,
444                BETA_LS,
445                numeric.sterel,
446                numeric.steabs,
447                SIGMA1,
448                SIGMA2,
449                numeric.epsrel,
450                numeric.epsabs,
451                tnls_scratch,
452                obj,
453            );
454            f = ls_res.f;
455            fcnt = ls_res.fcnt;
456            gcnt = ls_res.gcnt;
457            x.copy_from_slice(&tnls_scratch.xret);
458            g.copy_from_slice(&tnls_scratch.gret);
459
460            if ls_res.inform < 0 {
461                inform = ls_res.inform;
462                break;
463            }
464            inform = ls_res.inform;
465
466            // packmol behavior: if tnls stops with inform=6, discard TN step and force SPG.
467            if ls_res.inform == 6 {
468                let lamspg = if iter == 1 || sty <= 0.0 {
469                    F::max(1.0, xnorm) / gpeucn2.sqrt().max(positive_norm_floor())
470                } else {
471                    sts / sty
472                };
473                let lamspg = lamspg.clamp(LSPGMI, LSPGMA);
474
475                let spg_res = spg::spgls(
476                    n,
477                    x,
478                    g.as_slice(),
479                    l,
480                    u,
481                    lamspg,
482                    f,
483                    NINT,
484                    MININTERP,
485                    FMIN,
486                    params.maxfc,
487                    fcnt,
488                    GAMMA,
489                    SIGMA1,
490                    SIGMA2,
491                    numeric.sterel,
492                    numeric.steabs,
493                    numeric.epsrel,
494                    numeric.epsabs,
495                    spg_scratch,
496                    obj,
497                );
498                f = spg_res.f;
499                fcnt = spg_res.fcnt;
500                x.copy_from_slice(&spg_scratch.xtrial);
501
502                if spg_res.inform < 0 {
503                    inform = spg_res.inform;
504                    break;
505                }
506
507                let infotmp = spg_res.inform;
508                obj.evaluate(x, EvalMode::GradientOnly, Some(g.as_mut_slice()));
509                gcnt += 1;
510                inform = infotmp;
511            }
512        }
513
514        // Adjust to bounds near machine precision (packmol gencan.f lines 2363-2371).
515        for i in 0..n {
516            if x[i] <= l[i] + (numeric.epsrel * l[i].abs()).max(numeric.epsabs) {
517                x[i] = l[i];
518            } else if x[i] >= u[i] - (numeric.epsrel * u[i].abs()).max(numeric.epsabs) {
519                x[i] = u[i];
520            }
521        }
522
523        // Update x norm.
524        xnorm = x.iter().map(|xi| xi * xi).sum::<F>().sqrt();
525
526        // Update BB steplength: sts = (x-s)^T(x-s), sty = (x-s)^T(g-y)
527        sts = 0.0;
528        sty = 0.0;
529        for i in 0..n {
530            let ds = x[i] - s[i];
531            let dy = g[i] - y[i];
532            sts += ds * ds;
533            sty += ds * dy;
534        }
535
536        // Update projected gradient info (reuses pre-allocated ind buffer)
537        let pg = projected_gradient_info(n, x, g.as_slice(), l, u, ind);
538        gpsupn = pg.0;
539        gpeucn2 = pg.1;
540        gieucn2 = pg.2;
541        nind = pg.3;
542    }
543
544    // No extra compute_f here — packer.rs calls compute_f with unscaled radii
545    // immediately after pgencan returns, so this would be redundant.
546
547    GencanResult {
548        f,
549        gpsupn,
550        iter,
551        fcnt,
552        gcnt,
553        cgcnt,
554        inform,
555    }
556}
557
558/// Compute projected gradient info into pre-allocated `ind` buffer.
559/// Returns (gpsupn, gpeucn2, gieucn2, nind).
560fn projected_gradient_info(
561    n: usize,
562    x: &[F],
563    g: &[F],
564    l: &[F],
565    u: &[F],
566    ind: &mut Vec<usize>,
567) -> (F, F, F, usize) {
568    let mut gpsupn = 0.0 as F;
569    let mut gpeucn2 = 0.0 as F;
570    let mut gieucn2 = 0.0 as F;
571
572    ind.clear();
573    for i in 0..n {
574        let gpi = (x[i] - g[i]).clamp(l[i], u[i]) - x[i];
575        gpsupn = gpsupn.max(gpi.abs());
576        gpeucn2 += gpi * gpi;
577        if x[i] > l[i] && x[i] < u[i] {
578            gieucn2 += gpi * gpi;
579            ind.push(i);
580        }
581    }
582
583    (gpsupn, gpeucn2, gieucn2, ind.len())
584}
585
586/// Packmol precision check (`packmolprecision` in `pgencan.f90`):
587/// recompute objective-side violations and test `fdist/frest`.
588fn converged(obj: &dyn Objective, precision: F) -> bool {
589    obj.fdist() < precision && obj.frest() < precision
590}
591
592/// Compute scaling for CG epsilon.
593fn gp_ieee_signal(gpsupn: F, cgepsf: F, cgepsi: F, cggpnf: F) -> (F, F) {
594    if gpsupn > 0.0 {
595        let acgeps = (cgepsf / cgepsi).log10() / (cggpnf / gpsupn).log10();
596        let bcgeps = cgepsi.log10() - acgeps * gpsupn.log10();
597        (acgeps, bcgeps)
598    } else {
599        (0.0, cgepsf)
600    }
601}
602
603fn compute_cgeps(gpsupn: F, acgeps: F, bcgeps: F, cgepsf: F, cgepsi: F) -> F {
604    let cgeps = (10.0 as F).powf(acgeps * gpsupn.log10() + bcgeps);
605    cgeps.clamp(cgepsf, cgepsi)
606}
607
608/// Truncated-Newton line search result.
609struct LsResult {
610    pub f: F,
611    pub fcnt: usize,
612    pub gcnt: usize,
613    pub inform: i32,
614}
615
616/// Reusable buffers for TN line search (`tnls` in Packmol `gencan.f`).
617struct TnLsScratch {
618    xret: Vec<F>,
619    gret: Vec<F>,
620    xplus: Vec<F>,
621    xtmp: Vec<F>,
622    gplus: Vec<F>,
623}
624
625impl TnLsScratch {
626    fn new(n: usize) -> Self {
627        Self {
628            xret: vec![0.0; n],
629            gret: vec![0.0; n],
630            xplus: vec![0.0; n],
631            xtmp: vec![0.0; n],
632            gplus: vec![0.0; n],
633        }
634    }
635
636    fn ensure_len(&mut self, n: usize) {
637        if self.xret.len() != n {
638            self.xret.resize(n, 0.0);
639        }
640        if self.gret.len() != n {
641            self.gret.resize(n, 0.0);
642        }
643        if self.xplus.len() != n {
644            self.xplus.resize(n, 0.0);
645        }
646        if self.xtmp.len() != n {
647            self.xtmp.resize(n, 0.0);
648        }
649        if self.gplus.len() != n {
650            self.gplus.resize(n, 0.0);
651        }
652    }
653}
654
655#[allow(clippy::too_many_arguments)]
656#[allow(unused_assignments)]
657fn tn_linesearch(
658    nind: usize,
659    ind: &[usize],
660    n: usize,
661    x: &[F],
662    g: &[F],
663    d: &[F],
664    l: &[F],
665    u: &[F],
666    f0: F,
667    amax: F,
668    rbdtype: i32,
669    rbdind: usize,
670    nint: F,
671    next: F,
672    mininterp: usize,
673    maxextrap: usize,
674    fmin: F,
675    maxfc: usize,
676    mut fcnt: usize,
677    mut gcnt: usize,
678    gamma: F,
679    beta: F,
680    _sterel: F,
681    _steabs: F,
682    sigma1: F,
683    sigma2: F,
684    epsrel: F,
685    epsabs: F,
686    scratch: &mut TnLsScratch,
687    obj: &mut dyn Objective,
688) -> LsResult {
689    scratch.ensure_len(n);
690    let nind = nind.min(ind.len());
691    let xret = &mut scratch.xret;
692    let gret = &mut scratch.gret;
693    let xplus = &mut scratch.xplus;
694    let xtmp = &mut scratch.xtmp;
695    let gplus = &mut scratch.gplus;
696    xret.copy_from_slice(x);
697    gret.copy_from_slice(g);
698    let mut fret = f0;
699
700    let mut gplus_valid = false;
701
702    // gtd = <g,d> in the free-variable subspace.
703    let mut gtd = 0.0 as F;
704    for &ii in &ind[..nind] {
705        gtd += g[ii] * d[ii];
706    }
707
708    // First trial alpha = min(1, amax)
709    let mut alpha = (1.0 as F).min(amax);
710    xplus.copy_from_slice(x);
711    for &ii in &ind[..nind] {
712        xplus[ii] = x[ii] + alpha * d[ii];
713    }
714    if alpha == amax && rbdtype != 0 {
715        if rbdtype == 1 {
716            xplus[rbdind] = l[rbdind];
717        } else {
718            xplus[rbdind] = u[rbdind];
719        }
720    }
721
722    let mut fplus = obj.evaluate(xplus, EvalMode::FOnly, None).f_total;
723    fcnt += 1;
724
725    let mut do_extrap = false;
726
727    // Decide between extrapolation and interpolation.
728    if amax > 1.0 {
729        if fplus <= f0 + gamma * alpha * gtd {
730            obj.evaluate(xplus, EvalMode::GradientOnly, Some(gplus));
731            gcnt += 1;
732            gplus_valid = true;
733
734            let mut gptd = 0.0 as F;
735            for &ii in &ind[..nind] {
736                gptd += gplus[ii] * d[ii];
737            }
738
739            if gptd < beta * gtd {
740                do_extrap = true;
741            } else {
742                xret.copy_from_slice(xplus);
743                gret.copy_from_slice(gplus);
744                fret = fplus;
745                return LsResult {
746                    f: fret,
747                    fcnt,
748                    gcnt,
749                    inform: 0,
750                };
751            }
752        }
753    } else if fplus < f0 {
754        do_extrap = true;
755    }
756
757    // ------------------------------------------------------------------
758    // Extrapolation
759    // ------------------------------------------------------------------
760    if do_extrap {
761        let mut extrap = 0usize;
762
763        loop {
764            if fplus <= fmin {
765                xret.copy_from_slice(xplus);
766                fret = fplus;
767                if extrap != 0 || amax <= 1.0 {
768                    obj.evaluate(xret, EvalMode::GradientOnly, Some(gret));
769                    gcnt += 1;
770                } else if gplus_valid {
771                    gret.copy_from_slice(gplus);
772                }
773                return LsResult {
774                    f: fret,
775                    fcnt,
776                    gcnt,
777                    inform: 4,
778                };
779            }
780
781            if fcnt >= maxfc {
782                xret.copy_from_slice(xplus);
783                fret = fplus;
784                if extrap != 0 || amax <= 1.0 {
785                    obj.evaluate(xret, EvalMode::GradientOnly, Some(gret));
786                    gcnt += 1;
787                } else if gplus_valid {
788                    gret.copy_from_slice(gplus);
789                }
790                return LsResult {
791                    f: fret,
792                    fcnt,
793                    gcnt,
794                    inform: 8,
795                };
796            }
797
798            if extrap >= maxextrap {
799                xret.copy_from_slice(xplus);
800                fret = fplus;
801                if extrap != 0 || amax <= 1.0 {
802                    obj.evaluate(xret, EvalMode::GradientOnly, Some(gret));
803                    gcnt += 1;
804                } else if gplus_valid {
805                    gret.copy_from_slice(gplus);
806                }
807                return LsResult {
808                    f: fret,
809                    fcnt,
810                    gcnt,
811                    inform: 7,
812                };
813            }
814
815            let atmp = if alpha < amax && next * alpha > amax {
816                amax
817            } else {
818                next * alpha
819            };
820
821            xtmp.copy_from_slice(x);
822            for &ii in &ind[..nind] {
823                xtmp[ii] = x[ii] + atmp * d[ii];
824            }
825            if atmp == amax && rbdtype != 0 {
826                if rbdtype == 1 {
827                    xtmp[rbdind] = l[rbdind];
828                } else {
829                    xtmp[rbdind] = u[rbdind];
830                }
831            }
832            if atmp > amax {
833                for &ii in &ind[..nind] {
834                    xtmp[ii] = xtmp[ii].clamp(l[ii], u[ii]);
835                }
836            }
837
838            if alpha > amax {
839                let mut samep = true;
840                for &ii in &ind[..nind] {
841                    if (xtmp[ii] - xplus[ii]).abs() > (epsrel * xplus[ii].abs()).max(epsabs) {
842                        samep = false;
843                        break;
844                    }
845                }
846
847                if samep {
848                    xret.copy_from_slice(xplus);
849                    fret = fplus;
850                    if extrap != 0 || amax <= 1.0 {
851                        obj.evaluate(xret, EvalMode::GradientOnly, Some(gret));
852                        gcnt += 1;
853                    } else if gplus_valid {
854                        gret.copy_from_slice(gplus);
855                    }
856                    return LsResult {
857                        f: fret,
858                        fcnt,
859                        gcnt,
860                        inform: 0,
861                    };
862                }
863            }
864
865            let ftmp = obj.evaluate(xtmp, EvalMode::FOnly, None).f_total;
866            fcnt += 1;
867
868            if ftmp < fplus {
869                alpha = atmp;
870                fplus = ftmp;
871                xplus.copy_from_slice(xtmp);
872                gplus_valid = false;
873                extrap += 1;
874                continue;
875            }
876
877            xret.copy_from_slice(xplus);
878            fret = fplus;
879            if extrap != 0 || amax <= 1.0 {
880                obj.evaluate(xret, EvalMode::GradientOnly, Some(gret));
881                gcnt += 1;
882            } else if gplus_valid {
883                gret.copy_from_slice(gplus);
884            } else {
885                obj.evaluate(xret, EvalMode::GradientOnly, Some(gret));
886                gcnt += 1;
887            }
888            return LsResult {
889                f: fret,
890                fcnt,
891                gcnt,
892                inform: 0,
893            };
894        }
895    }
896
897    // ------------------------------------------------------------------
898    // Interpolation
899    // ------------------------------------------------------------------
900    let mut interp = 0usize;
901    loop {
902        if fplus <= fmin {
903            xret.copy_from_slice(xplus);
904            fret = fplus;
905            obj.evaluate(xret, EvalMode::GradientOnly, Some(gret));
906            gcnt += 1;
907            return LsResult {
908                f: fret,
909                fcnt,
910                gcnt,
911                inform: 4,
912            };
913        }
914
915        if fcnt >= maxfc {
916            if fplus < f0 {
917                xret.copy_from_slice(xplus);
918                fret = fplus;
919                obj.evaluate(xret, EvalMode::GradientOnly, Some(gret));
920                gcnt += 1;
921            }
922            return LsResult {
923                f: fret,
924                fcnt,
925                gcnt,
926                inform: 8,
927            };
928        }
929
930        if fplus <= f0 + gamma * alpha * gtd {
931            xret.copy_from_slice(xplus);
932            fret = fplus;
933            obj.evaluate(xret, EvalMode::GradientOnly, Some(gret));
934            gcnt += 1;
935            return LsResult {
936                f: fret,
937                fcnt,
938                gcnt,
939                inform: 0,
940            };
941        }
942
943        interp += 1;
944        if alpha < sigma1 {
945            alpha /= nint;
946        } else {
947            let denom = 2.0 * (fplus - f0 - alpha * gtd);
948            let atmp = if denom != 0.0 {
949                (-gtd * alpha * alpha) / denom
950            } else {
951                alpha / nint
952            };
953            if atmp < sigma1 || atmp > sigma2 * alpha {
954                alpha /= nint;
955            } else {
956                alpha = atmp;
957            }
958        }
959
960        xplus.copy_from_slice(x);
961        for &ii in &ind[..nind] {
962            xplus[ii] = x[ii] + alpha * d[ii];
963        }
964
965        fplus = obj.evaluate(xplus, EvalMode::FOnly, None).f_total;
966        fcnt += 1;
967
968        let mut samep = true;
969        for &ii in &ind[..nind] {
970            if (alpha * d[ii]).abs() > (epsrel * x[ii].abs()).max(epsabs) {
971                samep = false;
972                break;
973            }
974        }
975        if interp >= mininterp && samep {
976            return LsResult {
977                f: fret,
978                fcnt,
979                gcnt,
980                inform: 6,
981            };
982        }
983    }
984}