Skip to main content

burn_optim/optim/
lbfgs.rs

1#![allow(clippy::excessive_precision)]
2
3use burn_core as burn;
4
5use super::{GradientsParams, ParameterContext};
6use crate::{LearningRate, OptimizerRecord, RecordTensor};
7use crate::{RecordState, StateSink, StateSource};
8use burn::config::Config;
9use burn::module::{Module, ModuleMapper, ModuleVisitor, Param};
10use burn::store::RecordError;
11use burn::tensor::{Bytes, Device, Tensor};
12use serde::{Deserialize, Serialize};
13
14use alloc::vec;
15use alloc::vec::Vec;
16use core::ops::Range;
17#[cfg(not(feature = "std"))]
18#[allow(unused_imports)]
19use num_traits::Float as _;
20
21/// Cubic Interpolate
22///
23/// Uses two points (x1, f1), (x2, f2) and their first derivatives g1,g2 to construct
24/// a cubic interpolant and return its minimum within the given bounds.
25fn cubic_interpolate(
26    x1: f64,
27    f1: f64,
28    g1: f64,
29    x2: f64,
30    f2: f64,
31    g2: f64,
32    bounds: Option<(f64, f64)>,
33) -> f64 {
34    // Compute bounds of interpolation area
35    let (min_bound, max_bound) = bounds.unwrap_or(if x1 <= x2 { (x1, x2) } else { (x2, x1) });
36    // Code for most common case: cubic interpolation of 2 points
37    // with function and derivative values for both
38    // Solution in this case (where x2 is the farthest point)
39    // d1 = g1 + g2 - 3*(f1 - f2) / (x1-x2);
40    // d2 = sqrt(d1^2 - g1 * g2);
41    // min_pos = x2 - (x2 - x1)*((g2 + d2 - d1)/(g2 - g1 + 2*d2));
42    // t_new = min(max(min_pos,min_bound), max_bound);
43    let d1 = g1 + g2 - 3.0 * (f1 - f2) / (x1 - x2);
44    let d2_square = d1 * d1 - g1 * g2;
45
46    if d2_square >= 0.0 {
47        let d2 = d2_square.sqrt();
48        let min_pos = if x1 <= x2 {
49            x2 - (x2 - x1) * ((g2 + d2 - d1) / (g2 - g1 + 2.0 * d2))
50        } else {
51            x1 - (x1 - x2) * ((g1 + d2 - d1) / (g1 - g2 + 2.0 * d2))
52        };
53        min_pos.max(min_bound).min(max_bound)
54    } else {
55        (min_bound + max_bound) / 2.0
56    }
57}
58/// Auxiliary Struct For Strong_Wolfe
59struct LineSearchSample {
60    // step size
61    t: f64,
62    // loss
63    f: f64,
64    // gradient
65    g: FlatGradients,
66    // directional derivative
67    gtd: f64,
68}
69
70#[allow(clippy::too_many_arguments)]
71fn strong_wolfe<F>(
72    // obj_func(x,step size,direction) -> (loss,grad)
73    obj_func: &mut F,
74    x: &Tensor<1>,
75    // initial step size
76    mut t: f64,
77    d: &Tensor<1>,
78    f: f64,
79    g: FlatGradients,
80    gtd: f64,
81    c1: f64,
82    c2: f64,
83    tolerance_change: f64,
84    max_ls: usize,
85) -> (f64, FlatGradients, f64, usize)
86where
87    F: FnMut(&Tensor<1>, f64, &Tensor<1>) -> (f64, FlatGradients),
88{
89    let d_norm: f64 = d.clone().abs().max().into_scalar();
90
91    // evaluate objective and gradient using initial step
92    let (mut f_new, mut g_new) = obj_func(x, t, d);
93    let mut ls_func_evals = 1;
94    let mut gtd_new = g_new.tensor.clone().dot(d.clone()).into_scalar();
95
96    // bracket an interval [t_prev,t] containing a point satisfying the Wolfe criteria
97    let (mut t_prev, mut f_prev, mut g_prev, mut gtd_prev) = (0.0, f, g.clone(), gtd);
98    let mut done = false;
99    let mut ls_iter = 0;
100
101    // the interval [low,high] using for Zoom phase
102    let mut bracket: Option<[LineSearchSample; 2]> = None;
103    // point which satisfy the wolfe condition
104    let mut wolfe_bracket: Option<LineSearchSample> = None;
105    while ls_iter < max_ls {
106        // Checking Conditions.
107
108        // Checking the Armijo Condition and function value increasing condition.
109        // Armijo: f(x+t*d) <= f(x) + c_1 t gtd
110        if f_new > (f + c1 * t * gtd) || (ls_iter > 1 && f_new >= f_prev) {
111            bracket = Some([
112                LineSearchSample {
113                    t: t_prev,
114                    f: f_prev,
115                    g: g_prev,
116                    gtd: gtd_prev,
117                },
118                LineSearchSample {
119                    t,
120                    f: f_new,
121                    g: g_new.clone(),
122                    gtd: gtd_new,
123                },
124            ]);
125            break;
126        }
127
128        // Checking Strong Wolfe Condition
129        // |gtd_new| <= -c_2 gtd
130        if gtd_new.abs() <= -c2 * gtd {
131            wolfe_bracket = Some(LineSearchSample {
132                t,
133                f: f_new,
134                g: g_new.clone(),
135                gtd: gtd_new,
136            });
137            done = true;
138            break;
139        }
140
141        // gtd_new >=0 , there must be a local minimum in the interval.
142        if gtd_new >= 0.0 {
143            bracket = Some([
144                LineSearchSample {
145                    t: t_prev,
146                    f: f_prev,
147                    g: g_prev,
148                    gtd: gtd_prev,
149                },
150                LineSearchSample {
151                    t,
152                    f: f_new,
153                    g: g_new.clone(),
154                    gtd: gtd_new,
155                },
156            ]);
157            break;
158        }
159
160        // interpolate
161        let min_step = t + 0.01 * (t - t_prev);
162        let max_step = t * 10.0;
163        let t_next = cubic_interpolate(
164            t_prev,
165            f_prev,
166            gtd_prev,
167            t,
168            f_new,
169            gtd_new,
170            Some((min_step, max_step)),
171        );
172        t_prev = t;
173        f_prev = f_new;
174        g_prev = g_new;
175        gtd_prev = gtd_new;
176
177        // next step
178        t = t_next;
179        (f_new, g_new) = obj_func(x, t, d);
180        ls_func_evals += 1;
181        gtd_new = g_new.tensor.clone().dot(d.clone()).into_scalar();
182        ls_iter += 1;
183    }
184    if let Some(sample) = wolfe_bracket {
185        return (sample.f, sample.g, sample.t, ls_func_evals);
186    }
187
188    let mut bracket = bracket.unwrap_or_else(|| {
189        [
190            LineSearchSample {
191                t: 0.0,
192                f,
193                g: g.clone(),
194                gtd,
195            },
196            LineSearchSample {
197                t,
198                f: f_new,
199                g: g_new.clone(),
200                gtd: gtd_new,
201            },
202        ]
203    });
204
205    // zoom phase
206    let mut insuf_progress = false;
207
208    // find high and low points in bracket
209    let (mut low_idx, mut high_idx) = if bracket[0].f <= bracket[1].f {
210        (0, 1)
211    } else {
212        (1, 0)
213    };
214
215    while !done && ls_iter < max_ls {
216        let diff = (bracket[1].t - bracket[0].t).abs();
217        // line-search bracket is so small
218        if diff * d_norm < tolerance_change {
219            break;
220        }
221
222        // compute new trial value
223        t = cubic_interpolate(
224            bracket[0].t,
225            bracket[0].f,
226            bracket[0].gtd,
227            bracket[1].t,
228            bracket[1].f,
229            bracket[1].gtd,
230            None,
231        );
232
233        let b_min = bracket[0].t.min(bracket[1].t);
234        let b_max = bracket[0].t.max(bracket[1].t);
235        let eps = 0.1 * (b_max - b_min);
236
237        if (b_max - t).min(t - b_min) < eps {
238            // interpolation close to boundary
239            if insuf_progress || t >= b_max || t <= b_min {
240                t = if (t - b_max).abs() < (t - b_min).abs() {
241                    b_max - eps
242                } else {
243                    b_min + eps
244                };
245                insuf_progress = false;
246            } else {
247                insuf_progress = true;
248            }
249        } else {
250            insuf_progress = false;
251        }
252
253        // Evaluate new point
254        (f_new, g_new) = obj_func(x, t, d);
255
256        ls_func_evals += 1;
257        gtd_new = g_new.tensor.clone().dot(d.clone()).into_scalar();
258        ls_iter += 1;
259
260        let armijo_holds = f_new <= (f + c1 * t * gtd) && f_new < bracket[low_idx].f;
261
262        if !armijo_holds {
263            bracket[high_idx] = LineSearchSample {
264                t,
265                f: f_new,
266                g: g_new,
267                gtd: gtd_new,
268            };
269        } else {
270            if gtd_new.abs() <= -c2 * gtd {
271                return (f_new, g_new, t, ls_func_evals);
272            }
273
274            if gtd_new * (bracket[high_idx].t - bracket[low_idx].t) >= 0.0 {
275                bracket[high_idx] = LineSearchSample {
276                    t: bracket[low_idx].t,
277                    f: bracket[low_idx].f,
278                    g: bracket[low_idx].g.clone(),
279                    gtd: bracket[low_idx].gtd,
280                };
281            }
282            bracket[low_idx] = LineSearchSample {
283                t,
284                f: f_new,
285                g: g_new,
286                gtd: gtd_new,
287            };
288        }
289
290        if bracket[0].f <= bracket[1].f {
291            low_idx = 0;
292            high_idx = 1;
293        } else {
294            low_idx = 1;
295            high_idx = 0;
296        }
297    }
298    // return stuff
299    (
300        bracket[low_idx].f,
301        bracket[low_idx].g.clone(),
302        bracket[low_idx].t,
303        ls_func_evals,
304    )
305}
306
307/// Strategy for the line search optimization phase
308#[derive(Clone, Default, Debug, Copy, PartialEq, Eq, Serialize, Deserialize)]
309pub enum LineSearchFn {
310    /// No line search performed
311    #[default]
312    None,
313    /// strong wolfe conditions
314    ///
315    /// See: <https://en.wikipedia.org/wiki/Wolfe_conditions>
316    StrongWolfe,
317}
318
319/// LBFGS Configuration.
320#[derive(Config, Debug)]
321pub struct LBFGSConfig {
322    /// Maximal number of iterations per optimization step (default: 20)
323    #[config(default = 20)]
324    pub max_iter: usize,
325    /// Update history size (default: 100).
326    #[config(default = 100)]
327    pub history_size: usize,
328    /// Termination tolerance on first order optimality (default: 1e-7).
329    #[config(default = 1e-7)]
330    pub tolerance_grad: f64,
331    /// Termination tolerance on function value/parameter changes (default: 1e-9).
332    #[config(default = 1e-9)]
333    pub tolerance_change: f64,
334    /// Maximal number of function evaluations per optimization step (default: max_iter * 1.25).
335    #[config(default = "None")]
336    pub max_eval: Option<usize>,
337    /// Either ‘strong_wolfe’ or None (default: None).
338    #[config(default = "LineSearchFn::None")]
339    pub line_search_fn: LineSearchFn,
340}
341
342impl LBFGSConfig {
343    /// Initialize LBFGS optimizer.
344    ///
345    /// # Returns
346    ///
347    /// Returns an optimizer that can be used to optimize a module
348    pub fn init(&self) -> LBFGS {
349        // by default max_eval = max_iter * 5/4
350        let max_eval = self.max_eval.unwrap_or(self.max_iter * 5 / 4);
351        LBFGS {
352            config: LBFGSConfig {
353                max_iter: self.max_iter,
354                history_size: self.history_size,
355                tolerance_grad: self.tolerance_grad,
356                tolerance_change: self.tolerance_change,
357                max_eval: Some(max_eval),
358                line_search_fn: self.line_search_fn,
359            },
360            state: Default::default(),
361        }
362    }
363}
364
365#[derive(Clone)]
366struct FlatGradients {
367    tensor: Tensor<1>,
368    missing: Vec<Range<usize>>,
369}
370
371impl FlatGradients {
372    fn mask_direction(&self, mut direction: Tensor<1>) -> Tensor<1> {
373        assert_eq!(
374            direction.dims(),
375            self.tensor.dims(),
376            "L-BFGS search direction length must match the gradient length"
377        );
378        // History can couple parameters, giving a nonzero direction even where gradients are absent.
379        for range in &self.missing {
380            direction =
381                direction.slice_assign(range.clone(), self.tensor.clone().slice(range.clone()));
382        }
383        direction
384    }
385}
386
387/// Collects gradients in module visit order, retaining slots for missing gradients.
388struct FlattenGradsVisitorInner<'a> {
389    grads: &'a GradientsParams,
390    tensors: &'a mut Vec<Tensor<1>>,
391    missing: &'a mut Vec<Range<usize>>,
392    offset: usize,
393}
394
395impl ModuleVisitor for FlattenGradsVisitorInner<'_> {
396    fn visit_float<const D: usize>(&mut self, param: &Param<Tensor<D>>) {
397        let shape = param.lazy_shape();
398        let numel = shape.num_elements();
399        let grad = match self.grads.get::<D>(param.id) {
400            Some(grad) => {
401                assert_eq!(
402                    grad.shape(),
403                    shape,
404                    "L-BFGS gradient shape must match parameter {}",
405                    param.id
406                );
407                grad
408            }
409            None => {
410                if numel > 0 {
411                    self.missing.push(self.offset..self.offset + numel);
412                }
413                param.val().inner().dequantize().zeros_like()
414            }
415        };
416        self.tensors.push(grad.reshape([numel]));
417        self.offset += numel;
418    }
419}
420
421/// Flatten params to inner backend 1D tensor.
422fn flatten_params_inner<M: Module>(module: &M) -> Tensor<1> {
423    let mut tensors = Vec::new();
424    let mut visitor = FlattenParamsVisitorInner {
425        tensors: &mut tensors,
426    };
427    module.visit(&mut visitor);
428    if tensors.is_empty() {
429        return Tensor::empty([0], &module.devices()[0].clone().inner());
430    }
431    Tensor::cat(tensors, 0)
432}
433
434struct FlattenParamsVisitorInner<'a> {
435    tensors: &'a mut Vec<Tensor<1>>,
436}
437
438impl ModuleVisitor for FlattenParamsVisitorInner<'_> {
439    fn visit_float<const D: usize>(&mut self, param: &Param<Tensor<D>>) {
440        let t = param.val().inner();
441        let numel = t.shape().num_elements();
442        self.tensors.push(t.reshape([numel]));
443    }
444}
445
446/// Flatten gradients for a module.
447fn flatten_grads_inner<M: Module>(module: &M, grads: &GradientsParams) -> FlatGradients {
448    let mut tensors = Vec::new();
449    let mut missing = Vec::new();
450    let mut visitor = FlattenGradsVisitorInner {
451        grads,
452        tensors: &mut tensors,
453        missing: &mut missing,
454        offset: 0,
455    };
456    module.visit(&mut visitor);
457    let tensor = if tensors.is_empty() {
458        Tensor::empty([0], &module.devices()[0].clone().inner())
459    } else {
460        Tensor::cat(tensors, 0)
461    };
462    FlatGradients { tensor, missing }
463}
464
465/// Mapper that assigns each float param from a flat inner-backend 1D tensor.
466struct ParamsFromFlatMapperInner<'a> {
467    flat: &'a Tensor<1>,
468    offset: &'a mut usize,
469}
470
471impl ParamsFromFlatMapperInner<'_> {
472    fn take_slice(&mut self, numel: usize) -> Tensor<1> {
473        let start = *self.offset;
474        *self.offset += numel;
475        self.flat.clone().slice(start..*self.offset)
476    }
477}
478
479impl ModuleMapper for ParamsFromFlatMapperInner<'_> {
480    fn map_float<const D: usize>(&mut self, param: Param<Tensor<D>>) -> Param<Tensor<D>> {
481        let (id, tensor, mapper) = param.consume();
482        let numel = tensor.shape().num_elements();
483        let slice_1d = self.take_slice(numel);
484        let new_inner = slice_1d.reshape(tensor.shape());
485        let new_tensor = ParameterContext::capture(&tensor).restore(new_inner, id);
486        Param::from_mapped_value(id, new_tensor, mapper)
487    }
488}
489
490/// Overwrite module parameters from a flat inner-backend 1D tensor
491fn set_params_from_flat_inner<M: Module>(module: M, flat: Tensor<1>) -> M {
492    assert_eq!(
493        flat.dims(),
494        [module.num_params()],
495        "L-BFGS flat parameter length must match the module parameter count"
496    );
497    let mut offset = 0;
498    let mut mapper = ParamsFromFlatMapperInner {
499        flat: &flat,
500        offset: &mut offset,
501    };
502    module.map(&mut mapper)
503}
504
505/// L-BFGS optimizer state
506#[derive(Clone, RecordState)]
507pub struct LBFGSState {
508    /// Historical displacement vectors
509    pub history_s: Vec<Tensor<1>>,
510    /// Historical gradient difference vectors
511    pub history_y: Vec<Tensor<1>>,
512    /// Search direction
513    pub d: Option<Tensor<1>>,
514    /// Step size from the previous iteration
515    pub t: Option<f64>,
516    /// Flattened gradient from the previous iteration
517    pub prev_flat_grad: Option<Tensor<1>>,
518    /// Loss value from the previous iteration
519    pub prev_loss: Option<f64>,
520    /// Global iteration count
521    pub g_iter: usize,
522}
523
524impl LBFGSState {
525    fn check_num_params(&self, num_params: usize) {
526        for tensor in self
527            .history_s
528            .iter()
529            .chain(self.history_y.iter())
530            .chain(self.d.iter())
531            .chain(self.prev_flat_grad.iter())
532        {
533            assert_eq!(
534                tensor.dims(),
535                [num_params],
536                "L-BFGS state tensor length must match the module parameter count"
537            );
538        }
539    }
540
541    /// The device of the state's tensors, if any have been populated.
542    fn current_device(&self) -> Option<Device> {
543        self.prev_flat_grad
544            .as_ref()
545            .or(self.d.as_ref())
546            .or(self.history_s.first())
547            .map(|t| t.device())
548    }
549
550    /// Moves all historical tensors to the target device.
551    pub fn to_device(self, device: &Device) -> Self {
552        Self {
553            history_s: self
554                .history_s
555                .into_iter()
556                .map(|t| t.to_device(device))
557                .collect(),
558            history_y: self
559                .history_y
560                .into_iter()
561                .map(|t| t.to_device(device))
562                .collect(),
563            d: self.d.map(|t| t.to_device(device)),
564            t: self.t,
565            prev_flat_grad: self.prev_flat_grad.map(|t| t.to_device(device)),
566            prev_loss: self.prev_loss,
567            g_iter: self.g_iter,
568        }
569    }
570}
571impl Default for LBFGSState {
572    fn default() -> Self {
573        Self {
574            history_s: Vec::new(),
575            history_y: Vec::new(),
576            d: None,
577            t: Some(1.0),
578            prev_flat_grad: None,
579            prev_loss: None,
580            g_iter: 0,
581        }
582    }
583}
584
585/// L-BFGS optimizer.
586///
587/// Ported from [pytorch](https://github.com/pytorch/pytorch/torch/optim/lbfgs.py). Heavily inspired by [miniFunc](https://www.cs.ubc.ca/~schmidtm/Software/minFunc.html)
588///
589/// See also:
590/// - [L-BFGS](https://en.wikipedia.org/wiki/Limited-memory_BFGS)
591///
592/// # Note
593/// This optimizer is memory intensive
594#[derive(Clone)]
595pub struct LBFGS {
596    config: LBFGSConfig,
597    state: LBFGSState,
598}
599
600impl LBFGS {
601    /// Decompose the optimizer state into a serializable [`OptimizerRecord`] (burnpack format).
602    ///
603    /// L-BFGS keeps a single global state rather than per-parameter state, so its tensors are
604    /// named directly (e.g. `history_s.0`) and carry no parameter id.
605    pub fn to_record(&self) -> OptimizerRecord {
606        let mut sink = StateSink::default();
607        RecordState::state_flatten(&self.state, "", &mut sink);
608
609        let tensors = sink
610            .tensors
611            .into_iter()
612            .map(|(name, data)| RecordTensor {
613                name,
614                param_id: None,
615                data,
616            })
617            .collect();
618        let scalars = sink.scalars.into_iter().collect();
619
620        OptimizerRecord {
621            tensors,
622            scalars,
623            paths: Default::default(),
624        }
625    }
626
627    /// Load the optimizer state from an [`OptimizerRecord`].
628    ///
629    /// State tensors are materialized on the default device; the state is migrated to the gradient
630    /// device on the next [`step`](LBFGS::step), so no device argument is needed.
631    pub fn load_record(mut self, record: OptimizerRecord) -> Self {
632        let device = Device::default();
633        let mut source = StateSource::new(record.scalars);
634        for tensor in record.tensors {
635            source.insert_tensor(tensor.name, tensor.data);
636        }
637        if let Some(state) = LBFGSState::state_unflatten("", &mut source, &device) {
638            self.state = state;
639        }
640        self
641    }
642
643    /// Serialize the optimizer state to an in-memory burnpack byte buffer.
644    pub fn into_bytes(&self) -> Result<Bytes, RecordError> {
645        self.to_record().into_bytes()
646    }
647
648    /// Load the optimizer state from an in-memory burnpack byte buffer.
649    pub fn from_bytes(self, bytes: Bytes) -> Result<Self, RecordError> {
650        Ok(self.load_record(OptimizerRecord::from_bytes(bytes)?))
651    }
652
653    /// Save the optimizer state to a burnpack file on disk.
654    #[cfg(feature = "std")]
655    pub fn save<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), RecordError> {
656        self.to_record().save(path)
657    }
658
659    /// Load the optimizer state from a burnpack file on disk.
660    #[cfg(feature = "std")]
661    pub fn load<P: AsRef<std::path::Path>>(self, path: P) -> Result<Self, RecordError> {
662        Ok(self.load_record(OptimizerRecord::load(path)?))
663    }
664
665    /// A single optimization step for any tensor that represents the parameters of a model.
666    ///
667    /// Parameters without gradients are left unchanged. Their flattened gradient entries and search
668    /// direction entries are zero, including when the optimizer has accumulated history.
669    ///
670    /// # Panics
671    ///
672    /// Panics if a gradient shape does not match its parameter or saved state tensor lengths do not
673    /// match the module's parameter count.
674    pub fn step<M, F>(&mut self, lr: LearningRate, mut module: M, mut closure: F) -> (M, f64)
675    where
676        M: Module + Clone,
677        F: FnMut(M) -> (f64, GradientsParams),
678    {
679        // evaluate initial f(x) and df/dx
680        let (mut loss, grads) = closure(module.clone());
681        let mut current_evals = 1;
682
683        let mut flat_grad = flatten_grads_inner::<M>(&module, &grads);
684        let mut x_flat = flatten_params_inner::<M>(&module);
685        assert_eq!(
686            x_flat.dims(),
687            flat_grad.tensor.dims(),
688            "L-BFGS flat parameter and gradient lengths must match"
689        );
690        self.state.check_num_params(x_flat.dims()[0]);
691
692        if x_flat.dims()[0] == 0 {
693            return (module, loss);
694        }
695
696        // Migrate the state to the gradient's device when they differ (e.g. just after loading a
697        // record on the default device). This is a no-op once the state is built from gradients.
698        let device = flat_grad.tensor.device();
699        if self.state.current_device().is_some_and(|d| d != device) {
700            self.state = core::mem::take(&mut self.state).to_device(&device);
701        }
702
703        let opt_cond =
704            flat_grad.tensor.clone().abs().max().into_scalar::<f64>() <= self.config.tolerance_grad;
705        // optimal condition
706        if opt_cond {
707            return (module, loss);
708        }
709
710        // tensors cached in state
711        let mut d = self
712            .state
713            .d
714            .take()
715            .unwrap_or_else(|| flat_grad.tensor.clone().neg());
716        let mut t = self.state.t.unwrap_or(lr);
717        let mut prev_flat_grad = self.state.prev_flat_grad.take();
718
719        let mut n_iter = 0;
720
721        // optimize for a max of max_iter iterations
722        while n_iter < self.config.max_iter {
723            // keep track of nb of iterations
724            n_iter += 1;
725            self.state.g_iter += 1;
726
727            // compute gradient descent direction
728            if self.state.g_iter == 1 {
729                d = flat_grad.tensor.clone().neg();
730                self.state.history_s.clear();
731                self.state.history_y.clear();
732            } else {
733                // do lbfgs update (update memory)
734                if let Some(pg) = prev_flat_grad.as_ref() {
735                    let y = flat_grad.tensor.clone().sub(pg.clone());
736                    let s = d.clone().mul_scalar(t);
737
738                    let ys: f64 = y.clone().dot(s.clone()).into_scalar();
739
740                    if ys > 1e-10 {
741                        // updating memory
742                        if self.state.history_s.len() >= self.config.history_size {
743                            // shift history by one (limited-memory)
744                            self.state.history_s.remove(0);
745                            self.state.history_y.remove(0);
746                        }
747                        self.state.history_s.push(s);
748                        self.state.history_y.push(y);
749                    }
750                }
751
752                // compute the approximate (L-BFGS) inverse Hessian
753                // multiplied by the gradient
754                let num_old = self.state.history_s.len();
755                let mut q = flat_grad.tensor.clone().neg();
756                let mut alphas: Vec<Tensor<1>> =
757                    vec![Tensor::zeros([1], &flat_grad.tensor.device().inner()); num_old];
758
759                if num_old > 0 {
760                    // multiply by initial Hessian
761                    // r/d is the final direction
762                    for i in (0..num_old).rev() {
763                        let s = &self.state.history_s[i];
764                        let y = &self.state.history_y[i];
765                        let rho = y.clone().dot(s.clone()).powf_scalar(-1.0);
766                        let alpha = rho.clone().mul(s.clone().dot(q.clone()));
767                        alphas[i] = alpha.clone();
768                        q = q.sub(y.clone().mul(alpha));
769                    }
770
771                    let last_s = &self.state.history_s[num_old - 1];
772                    let last_y = &self.state.history_y[num_old - 1];
773                    let ys = last_y.clone().dot(last_s.clone());
774                    let yy = last_y.clone().dot(last_y.clone());
775                    let h_diag = ys.div(yy);
776
777                    let mut r = q.mul(h_diag);
778
779                    for ((s, y), alpha) in self
780                        .state
781                        .history_s
782                        .iter()
783                        .zip(self.state.history_y.iter())
784                        .zip(alphas)
785                        .take(num_old)
786                    {
787                        let rho = y.clone().dot(s.clone()).powf_scalar(-1.0);
788
789                        let beta = rho.mul(y.clone().dot(r.clone()));
790
791                        r = r.add(s.clone().mul(alpha.sub(beta)));
792                    }
793                    d = r;
794                } else {
795                    d = q;
796                }
797            }
798
799            d = flat_grad.mask_direction(d);
800            prev_flat_grad = Some(flat_grad.tensor.clone());
801            let prev_loss_iter = loss;
802
803            // compute step len
804            if self.state.g_iter == 1 {
805                let grad_l1: f64 = flat_grad.tensor.clone().abs().sum().into_scalar();
806                t = (1.0f64 / grad_l1).min(1.0) * lr;
807            } else {
808                t = lr;
809            }
810
811            // directional derivative
812            let gtd = flat_grad.tensor.clone().dot(d.clone()).into_scalar();
813
814            if gtd > -self.config.tolerance_change {
815                break;
816            }
817
818            let ls_func_evals;
819
820            if let LineSearchFn::StrongWolfe = self.config.line_search_fn {
821                // perform line search, using user function
822                let mut obj_func = |current_x: &Tensor<1>, step: f64, dir: &Tensor<1>| {
823                    let update = dir.clone().mul_scalar(step);
824                    let new_x = current_x.clone().add(update);
825                    let tmp_module = set_params_from_flat_inner::<M>(module.clone(), new_x);
826                    let (l, g) = closure(tmp_module);
827                    (l, flatten_grads_inner::<M>(&module, &g))
828                };
829
830                let (ls_f, ls_g, ls_t, evals) = strong_wolfe(
831                    &mut obj_func,
832                    &x_flat,
833                    t,
834                    &d,
835                    loss,
836                    flat_grad.clone(),
837                    gtd,
838                    1e-4,
839                    0.9,
840                    self.config.tolerance_change,
841                    self.config.max_eval.unwrap() - current_evals,
842                );
843
844                loss = ls_f;
845                flat_grad = ls_g;
846                t = ls_t;
847                ls_func_evals = evals;
848
849                x_flat = x_flat.add(d.clone().mul_scalar(t));
850                module = set_params_from_flat_inner::<M>(module, x_flat.clone());
851            } else {
852                // no line search, simply move with fixed-step
853                let step_vec = d.clone().mul_scalar(t);
854                x_flat = x_flat.add(step_vec);
855                module = set_params_from_flat_inner::<M>(module, x_flat.clone());
856                // re-evaluate function only if not in last iteration
857                // the reason we do this: in a stochastic setting,
858                // no use to re-evaluate that function here
859                let (new_loss, new_grads) = closure(module.clone());
860                loss = new_loss;
861                flat_grad = flatten_grads_inner::<M>(&module, &new_grads);
862                ls_func_evals = 1;
863            }
864
865            // update func eval
866            current_evals += ls_func_evals;
867
868            // check conditions
869
870            if current_evals >= self.config.max_eval.unwrap() {
871                break;
872            }
873
874            if flat_grad.tensor.clone().abs().max().into_scalar::<f64>()
875                <= self.config.tolerance_grad
876            {
877                break;
878            }
879
880            if d.clone().mul_scalar(t).abs().max().into_scalar::<f64>()
881                <= self.config.tolerance_change
882            {
883                break;
884            }
885
886            if (loss - prev_loss_iter).abs() < self.config.tolerance_change {
887                break;
888            }
889        }
890        self.state.d = Some(d);
891        self.state.t = Some(t);
892        self.state.prev_flat_grad = prev_flat_grad;
893        self.state.prev_loss = Some(loss);
894        (module, loss)
895    }
896    /// Moves the optimizer state to the specified device.
897    pub fn to_device(self, device: &Device) -> Self {
898        Self {
899            config: self.config,
900            // History tensors reside in InnerBackend, so we convert the device accordingly
901            state: self.state.to_device(device),
902        }
903    }
904}
905
906#[cfg(test)]
907mod tests {
908
909    use super::*;
910    use crate::GradientsParams;
911    use burn::module::Param;
912    use burn::tensor::{Tensor, TensorData};
913    use burn_nn::Linear;
914
915    fn given_linear_layer(weight: TensorData, bias: TensorData, device: &Device) -> Linear {
916        Linear {
917            weight: Param::from_data(weight, device),
918            bias: Some(Param::from_data(bias, device)),
919        }
920    }
921    #[test]
922    fn test_cubic_interpolate() {
923        let tolerance = 1e-8;
924
925        // basic
926        let (x1, f1, g1, x2, f2, g2) = (-1.0, 1.0, -2.0, 1.0, 1.0, 2.0);
927        let result = cubic_interpolate(x1, f1, g1, x2, f2, g2, None);
928        assert!(
929            (result - 0.00000).abs() < tolerance,
930            "Basic: Result {} should be close to 0.0",
931            result
932        );
933
934        // bound
935        let (x1, f1, g1, x2, f2, g2) = (0.0, 0.25, -1.0, 1.0, 0.25, 1.0);
936        let bounds = Some((0.6, 1.0));
937        let result = cubic_interpolate(x1, f1, g1, x2, f2, g2, bounds);
938        assert!(
939            (result - 0.6000000000).abs() < tolerance,
940            "Bound: Result {} should be clamped to 0.6",
941            result
942        );
943
944        // d2_square < 0,should return mid value
945        let (x1, f1, g1, x2, f2, g2) = (0.0, 0.0, 10.0, 1.0, 5.0, 10.0);
946        let result = cubic_interpolate(x1, f1, g1, x2, f2, g2, Some((0.0, 1.0)));
947        assert!(
948            (result - 0.5000000).abs() < tolerance,
949            "Fallback: Result {} should be midpoint 0.5",
950            result
951        );
952
953        // asymmetric
954        let (x1, f1, g1, x2, f2, g2) = (0.0, 1.0, -5.0, 1.0, 0.5, 1.0);
955        let result = cubic_interpolate(x1, f1, g1, x2, f2, g2, None);
956        assert!(
957            (result - 0.4606553370833684).abs() < tolerance,
958            "Asymmetric: Result {} should be 0.4606553370833684",
959            result
960        );
961
962        // not good value
963        let (x1, f1, g1, x2, f2, g2) = (
964            1.231232145,
965            -0.12567458754,
966            9.1231243007,
967            8.239105015,
968            -100.9012398021,
969            123201321.0293982,
970        );
971        let result_1 = cubic_interpolate(x1, f1, g1, x2, f2, g2, None);
972        let result_2 = cubic_interpolate(x1, f1, g1, x2, f2, g2, Some((-4.4, 4.4)));
973        assert!(
974            (result_1 - 5.9031480234724434).abs() < tolerance,
975            "not good value 1: Result {} should be 5.9031480234724434",
976            result
977        );
978        assert!(
979            (result_2 - 4.4000000000000004).abs() < tolerance,
980            "not good value 2: Result {} should be 4.4000000000000004",
981            result
982        );
983    }
984    #[test]
985    fn test_strong_wolfe_direct_comparison() {
986        let device = Device::default().autodiff();
987        let tol = 1e-6;
988
989        {
990            let x = Tensor::<1>::from_floats([2.1321912957_f64], &device);
991            let d = Tensor::<1>::from_floats([0.91312321_f64], &device);
992            let t_initial = 1.213132_f64;
993            fn func(x_base: &Tensor<1>, t_val: f64, d_vec: &Tensor<1>) -> (f64, FlatGradients) {
994                let curr_x = x_base.clone().add(d_vec.clone().mul_scalar(t_val));
995                let x2 = curr_x.clone().mul(curr_x.clone());
996                let x3 = x2.clone().mul(curr_x.clone());
997                let x4 = x2.clone().mul(x2.clone());
998
999                // f(x) = x^4 - 2*x^2 + x
1000                let f_elements = x4 - x2.mul_scalar(2.0) + curr_x.clone();
1001
1002                let f_val = f_elements.sum().into_scalar();
1003
1004                // g(x) = 4*x^3 - 4*x + 1
1005                let g = x3.mul_scalar(4.0) - curr_x.clone().mul_scalar(4.0)
1006                    + Tensor::ones_like(&curr_x);
1007
1008                (
1009                    f_val,
1010                    FlatGradients {
1011                        tensor: g,
1012                        missing: Vec::new(),
1013                    },
1014                )
1015            }
1016            let (f_init, g_init) = func(&x, 0.0, &d);
1017            let gtd_init = g_init.tensor.clone().dot(d.clone()).into_scalar::<f64>();
1018            println!("Initial State: f={},gtd = {}", f_init, gtd_init);
1019            assert!((f_init - 13.7080059052).abs() < tol);
1020            assert!((gtd_init - 28.5305728912).abs() < tol);
1021            let mut obj_func = |xb: &Tensor<1>, tv: f64, dv: &Tensor<1>| func(xb, tv, dv);
1022
1023            let (f_final, _g_final, t_final, evals) = strong_wolfe(
1024                &mut obj_func,
1025                &x,
1026                t_initial,
1027                &d,
1028                f_init,
1029                g_init,
1030                gtd_init,
1031                1e-4, // c1
1032                0.9,  // c2
1033                1e-9, // tolerance_change
1034                10,   // max_ls
1035            );
1036            let g_f = _g_final.tensor.into_scalar::<f64>();
1037            println!(
1038                "f_final:{:?},_g_final:{:?},t_final:{:?},evals:{:?}",
1039                f_final, g_f, t_final, evals
1040            );
1041            assert!((f_final - 13.708005905151367).abs() < tol);
1042            assert!((g_f - 31.2450428009).abs() < tol);
1043            assert!((t_final - 0.0).abs() < tol);
1044            assert!((evals == 11));
1045        }
1046    }
1047    #[test]
1048    fn test_lbfgs_strong_wolfe_comparison() {
1049        let device = Device::default().autodiff();
1050        let tol = 1e-5;
1051        let x_data = Tensor::<2>::from_data([[1.0], [2.0], [3.0]], &device);
1052        let y_true = Tensor::<2>::from_data([[3.0], [5.0], [7.0]], &device);
1053        let weight = TensorData::from([[0.5f64]]);
1054        let bias = TensorData::from([0.1f64]);
1055        let module = given_linear_layer(weight, bias, &device);
1056
1057        let mut optimizer = LBFGSConfig::new()
1058            .with_line_search_fn(LineSearchFn::StrongWolfe)
1059            .init();
1060        let mut closure = |mod_in: Linear| {
1061            let output = mod_in.forward(x_data.clone());
1062            let loss = burn_nn::loss::MseLoss::new().forward(
1063                output,
1064                y_true.clone(),
1065                burn_nn::loss::Reduction::Sum,
1066            );
1067
1068            let grads = loss.backward();
1069            let grads_params = GradientsParams::from_grads(grads, &mod_in);
1070
1071            (loss.into_scalar::<f64>(), grads_params)
1072        };
1073        let initial_loss = closure(module.clone()).0;
1074        assert!((initial_loss - 50.1300048828).abs() < tol);
1075        let (updated_module, final_loss) = optimizer.step(0.001, module, &mut closure);
1076        assert!((final_loss - 0.0234732367).abs() < tol);
1077        let optimized_data: f64 = updated_module.weight.val().into_scalar();
1078        let optimized_bias: f64 = updated_module.bias.as_ref().unwrap().val().into_scalar();
1079        assert!((optimized_data - 2.0570652485).abs() < tol);
1080        assert!((optimized_bias - 0.8106800914).abs() < tol);
1081    }
1082
1083    // A burnpack round-trip of the L-BFGS state (which holds `Vec<Tensor>` history buffers, optional
1084    // tensors and optional scalars) must restore enough that a further step agrees with the original.
1085    #[test]
1086    fn test_lbfgs_burnpack_round_trip() {
1087        let device = Device::default().autodiff();
1088        let tol = 1e-6;
1089        let x_data = Tensor::<2>::from_data([[1.0], [2.0], [3.0]], &device);
1090        let y_true = Tensor::<2>::from_data([[3.0], [5.0], [7.0]], &device);
1091        let module = given_linear_layer(
1092            TensorData::from([[0.5f64]]),
1093            TensorData::from([0.1f64]),
1094            &device,
1095        );
1096
1097        let make_closure = || {
1098            let x = x_data.clone();
1099            let y = y_true.clone();
1100            move |mod_in: Linear| {
1101                let output = mod_in.forward(x.clone());
1102                let loss = burn_nn::loss::MseLoss::new().forward(
1103                    output,
1104                    y.clone(),
1105                    burn_nn::loss::Reduction::Sum,
1106                );
1107                let grads = loss.backward();
1108                let grads_params = GradientsParams::from_grads(grads, &mod_in);
1109                (loss.into_scalar::<f64>(), grads_params)
1110            }
1111        };
1112
1113        let mut optimizer = LBFGSConfig::new()
1114            .with_line_search_fn(LineSearchFn::StrongWolfe)
1115            .init();
1116        let (module, _) = optimizer.step(0.001, module, &mut make_closure());
1117
1118        // Round-trip the optimizer state. State tensors live on the inner (non-autodiff) backend.
1119        let bytes = optimizer.into_bytes().unwrap();
1120        let mut reloaded = LBFGSConfig::new()
1121            .with_line_search_fn(LineSearchFn::StrongWolfe)
1122            .init()
1123            .from_bytes(bytes)
1124            .unwrap();
1125
1126        // A further identical step on each optimizer must agree — exercising the restored history.
1127        let (_, loss_original) = optimizer.step(0.001, module.clone(), &mut make_closure());
1128        let (_, loss_reloaded) = reloaded.step(0.001, module, &mut make_closure());
1129        assert!(
1130            (loss_original - loss_reloaded).abs() < tol,
1131            "losses differ after burnpack round-trip: {loss_original} vs {loss_reloaded}"
1132        );
1133    }
1134
1135    #[test]
1136    fn test_lbfgs_no_strong_wolfe_comparison() {
1137        let device = Device::default().autodiff();
1138        let tol = 1e-5;
1139        let x_data = Tensor::<2>::from_data([[1.0], [2.0], [3.0]], &device);
1140        let y_true = Tensor::<2>::from_data([[3.0], [5.0], [7.0]], &device);
1141        let weight = TensorData::from([[0.5f64]]);
1142        let bias = TensorData::from([0.1f64]);
1143        let module = given_linear_layer(weight, bias, &device);
1144
1145        let mut optimizer = LBFGSConfig::new()
1146            .with_line_search_fn(LineSearchFn::None)
1147            .init();
1148        let mut closure = |mod_in: Linear| {
1149            let output = mod_in.forward(x_data.clone());
1150            let loss = burn_nn::loss::MseLoss::new().forward(
1151                output,
1152                y_true.clone(),
1153                burn_nn::loss::Reduction::Sum,
1154            );
1155
1156            let grads = loss.backward();
1157            let grads_params = GradientsParams::from_grads(grads, &mod_in);
1158
1159            (loss.into_scalar::<f64>(), grads_params)
1160        };
1161        let initial_loss = closure(module.clone()).0;
1162        assert!((initial_loss - 50.1300048828).abs() < tol);
1163        let (updated_module, final_loss) = optimizer.step(0.001, module, &mut closure);
1164        assert!((final_loss - 48.2181930542).abs() < tol);
1165        let optimized_data: f64 = updated_module.weight.val().into_scalar();
1166        let optimized_bias: f64 = updated_module.bias.as_ref().unwrap().val().into_scalar();
1167
1168        assert!((optimized_data - 0.5302446192).abs() < tol);
1169        assert!((optimized_bias - 0.1142520783).abs() < tol);
1170    }
1171
1172    #[test]
1173    fn test_lbfgs_missing_gradients_preserve_parameter_positions() {
1174        let device = Device::default().autodiff();
1175        let values = [[1.0f32, 2.0], [3.0, 4.0], [5.0, 6.0]];
1176
1177        for line_search in [LineSearchFn::None, LineSearchFn::StrongWolfe] {
1178            for frozen in [false, true] {
1179                for missing in 0..values.len() {
1180                    let module: Vec<Param<Tensor<1>>> = values
1181                        .iter()
1182                        .enumerate()
1183                        .map(|(i, value)| {
1184                            let param = Param::from_tensor(Tensor::from_floats(*value, &device));
1185                            if frozen && i == missing {
1186                                param.freeze()
1187                            } else {
1188                                param
1189                            }
1190                        })
1191                        .collect();
1192                    let mut optimizer = LBFGSConfig::new()
1193                        .with_max_iter(1)
1194                        .with_line_search_fn(line_search)
1195                        .init();
1196                    let (module, _) = optimizer.step(1.0, module, |module| {
1197                        let loss = module
1198                            .iter()
1199                            .enumerate()
1200                            .filter(|(i, _)| frozen || *i != missing)
1201                            .map(|(_, param)| param.val().powf_scalar(2.0).sum())
1202                            .reduce(|a, b| a + b)
1203                            .unwrap();
1204                        let grads = GradientsParams::from_grads(loss.backward(), &module);
1205                        (loss.into_scalar::<f64>(), grads)
1206                    });
1207
1208                    let active_sum: f32 = values
1209                        .iter()
1210                        .enumerate()
1211                        .filter(|(i, _)| *i != missing)
1212                        .flat_map(|(_, value)| value)
1213                        .sum();
1214                    for (i, param) in module.iter().enumerate() {
1215                        let expected = values[i].map(|value| {
1216                            if i == missing {
1217                                value
1218                            } else {
1219                                value - value / active_sum
1220                            }
1221                        });
1222                        param.val().into_data().assert_approx_eq::<f32>(
1223                            &TensorData::from(expected),
1224                            Default::default(),
1225                        );
1226                        assert_eq!(param.is_require_grad(), !frozen || i != missing);
1227                    }
1228                }
1229            }
1230        }
1231    }
1232
1233    #[test]
1234    fn test_lbfgs_missing_scalar_gradient_does_not_broadcast() {
1235        let device = Device::default().autodiff();
1236        for line_search in [LineSearchFn::None, LineSearchFn::StrongWolfe] {
1237            let module = vec![
1238                Param::from_tensor(Tensor::<1>::from_floats([10.0], &device)).freeze(),
1239                Param::from_tensor(Tensor::<1>::from_floats([20.0], &device)),
1240            ];
1241            let mut optimizer = LBFGSConfig::new()
1242                .with_max_iter(1)
1243                .with_line_search_fn(line_search)
1244                .init();
1245            let (module, _) = optimizer.step(1.0, module, |module| {
1246                let loss = module[1].val().powf_scalar(2.0).sum();
1247                let grads = GradientsParams::from_grads(loss.backward(), &module);
1248                (loss.into_scalar::<f64>(), grads)
1249            });
1250
1251            assert_eq!(module[0].val().into_scalar::<f32>(), 10.0);
1252            assert_eq!(module[1].val().into_scalar::<f32>(), 19.0);
1253        }
1254    }
1255
1256    #[test]
1257    fn test_lbfgs_history_does_not_move_parameters_without_gradients() {
1258        let device = Device::default().autodiff();
1259        let coupled_loss = |module: Vec<Param<Tensor<1>>>| {
1260            let loss = (module[0].val() + module[1].val()).powf_scalar(2.0)
1261                + module[1].val().powf_scalar(2.0);
1262            let grads = GradientsParams::from_grads(loss.backward(), &module);
1263            (loss.into_scalar::<f64>(), grads)
1264        };
1265
1266        for line_search in [LineSearchFn::None, LineSearchFn::StrongWolfe] {
1267            for frozen in [false, true] {
1268                let mut module = vec![
1269                    Param::from_tensor(Tensor::<1>::from_floats([1.0], &device)),
1270                    Param::from_tensor(Tensor::<1>::from_floats([2.0], &device)),
1271                ];
1272                let mut optimizer = LBFGSConfig::new()
1273                    .with_max_iter(1)
1274                    .with_max_eval(Some(20))
1275                    .with_line_search_fn(line_search)
1276                    .init();
1277                (module, _) = optimizer.step(0.1, module, coupled_loss);
1278                (module, _) = optimizer.step(0.1, module, coupled_loss);
1279                assert!(!optimizer.state.history_s.is_empty());
1280
1281                if frozen {
1282                    module[0] = module[0].clone().freeze();
1283                }
1284                let unchanged = module[0].val().into_scalar::<f32>();
1285                let mut closure = |module: Vec<Param<Tensor<1>>>| {
1286                    if frozen {
1287                        coupled_loss(module)
1288                    } else {
1289                        let loss = module[1].val().powf_scalar(2.0);
1290                        let grads = GradientsParams::from_grads(loss.backward(), &module);
1291                        (loss.into_scalar::<f64>(), grads)
1292                    }
1293                };
1294                let initial_loss = closure(module.clone()).0;
1295                let (module, final_loss) = optimizer.step(0.1, module, &mut closure);
1296
1297                assert_eq!(module[0].val().into_scalar::<f32>(), unchanged);
1298                assert!(final_loss < initial_loss);
1299            }
1300        }
1301    }
1302
1303    #[test]
1304    fn test_lbfgs_no_gradients_leaves_parameters_unchanged() {
1305        let device = Device::default().autodiff();
1306        let module = Param::from_tensor(
1307            Tensor::<1>::from_floats([1.0, 2.0], &device).cast(burn::tensor::DType::F64),
1308        )
1309        .freeze();
1310        let mut optimizer = LBFGSConfig::new().init();
1311        let (module, _) = optimizer.step(1.0, module, |_| (0.0, GradientsParams::new()));
1312
1313        assert_eq!(optimizer.state.g_iter, 0);
1314        assert_eq!(module.val().dtype(), burn::tensor::DType::F64);
1315        module
1316            .val()
1317            .into_data()
1318            .assert_eq(&TensorData::from([1.0f64, 2.0]), false);
1319    }
1320
1321    #[test]
1322    #[should_panic(expected = "L-BFGS gradient shape must match parameter")]
1323    fn test_lbfgs_rejects_incorrect_gradient_shape() {
1324        let device = Device::default().autodiff();
1325        let module = Param::from_tensor(Tensor::<1>::from_floats([1.0, 2.0], &device));
1326        let mut optimizer = LBFGSConfig::new().init();
1327        optimizer.step(1.0, module, |module| {
1328            let mut grads = GradientsParams::new();
1329            grads.register(module.id, Tensor::<1>::ones([1], &device.clone().inner()));
1330            (1.0, grads)
1331        });
1332    }
1333
1334    #[test]
1335    #[should_panic(expected = "L-BFGS state tensor length must match the module parameter count")]
1336    fn test_lbfgs_rejects_incorrect_state_length() {
1337        let device = Device::default().autodiff();
1338        let module = Param::from_tensor(Tensor::<1>::from_floats([1.0, 2.0], &device));
1339        let mut optimizer = LBFGSConfig::new().init();
1340        optimizer.state.d = Some(Tensor::ones([1], &device.clone().inner()));
1341        optimizer.step(1.0, module, |module| {
1342            let loss = module.val().powf_scalar(2.0).sum();
1343            let grads = GradientsParams::from_grads(loss.backward(), &module);
1344            (loss.into_scalar::<f64>(), grads)
1345        });
1346    }
1347}