feos_dft/
solver.rs

1use crate::{
2    DFTProfile, FunctionalContribution, HelmholtzEnergyFunctional, WeightFunction,
3    WeightFunctionShape,
4};
5use feos_core::{FeosError, FeosResult, ReferenceSystem, Verbosity, log_iter, log_result};
6use nalgebra::{DMatrix, DVector, dvector};
7use ndarray::RemoveAxis;
8use ndarray::prelude::*;
9use petgraph::Directed;
10use petgraph::graph::Graph;
11use petgraph::visit::EdgeRef;
12use quantity::{SECOND, Time};
13use std::collections::VecDeque;
14use std::fmt;
15use std::ops::AddAssign;
16use std::time::{Duration, Instant};
17
18const DEFAULT_PARAMS_PICARD: PicardIteration = PicardIteration {
19    log: false,
20    max_iter: 500,
21    tol: 1e-11,
22    damping_coefficient: None,
23};
24const DEFAULT_PARAMS_ANDERSON_LOG: AndersonMixing = AndersonMixing {
25    log: true,
26    max_iter: 50,
27    tol: 1e-5,
28    damping_coefficient: 0.15,
29    mmax: 100,
30};
31const DEFAULT_PARAMS_ANDERSON: AndersonMixing = AndersonMixing {
32    log: false,
33    max_iter: 150,
34    tol: 1e-11,
35    damping_coefficient: 0.15,
36    mmax: 100,
37};
38const DEFAULT_PARAMS_NEWTON: Newton = Newton {
39    log: false,
40    max_iter: 50,
41    max_iter_gmres: 200,
42    tol: 1e-11,
43};
44
45#[derive(Clone, Copy, Debug)]
46struct PicardIteration {
47    log: bool,
48    max_iter: usize,
49    tol: f64,
50    damping_coefficient: Option<f64>,
51}
52
53#[derive(Clone, Copy, Debug)]
54struct AndersonMixing {
55    log: bool,
56    max_iter: usize,
57    tol: f64,
58    damping_coefficient: f64,
59    mmax: usize,
60}
61
62#[derive(Clone, Copy, Debug)]
63struct Newton {
64    log: bool,
65    max_iter: usize,
66    max_iter_gmres: usize,
67    tol: f64,
68}
69
70#[derive(Clone, Copy)]
71enum DFTAlgorithm {
72    PicardIteration(PicardIteration),
73    AndersonMixing(AndersonMixing),
74    Newton(Newton),
75}
76
77/// Settings for the DFT solver.
78#[derive(Clone)]
79pub struct DFTSolver {
80    algorithms: Vec<DFTAlgorithm>,
81    pub verbosity: Verbosity,
82}
83
84impl Default for DFTSolver {
85    fn default() -> Self {
86        Self {
87            algorithms: vec![
88                DFTAlgorithm::AndersonMixing(DEFAULT_PARAMS_ANDERSON_LOG),
89                DFTAlgorithm::AndersonMixing(DEFAULT_PARAMS_ANDERSON),
90            ],
91            verbosity: Default::default(),
92        }
93    }
94}
95
96impl DFTSolver {
97    pub fn new(verbosity: Option<Verbosity>) -> Self {
98        Self {
99            algorithms: vec![],
100            verbosity: verbosity.unwrap_or_default(),
101        }
102    }
103
104    pub fn picard_iteration(
105        mut self,
106        log: Option<bool>,
107        max_iter: Option<usize>,
108        tol: Option<f64>,
109        damping_coefficient: Option<f64>,
110    ) -> Self {
111        let mut params = DEFAULT_PARAMS_PICARD;
112        params.log = log.unwrap_or(params.log);
113        params.max_iter = max_iter.unwrap_or(params.max_iter);
114        params.tol = tol.unwrap_or(params.tol);
115        params.damping_coefficient = damping_coefficient;
116        self.algorithms.push(DFTAlgorithm::PicardIteration(params));
117        self
118    }
119
120    pub fn anderson_mixing(
121        mut self,
122        log: Option<bool>,
123        max_iter: Option<usize>,
124        tol: Option<f64>,
125        damping_coefficient: Option<f64>,
126        mmax: Option<usize>,
127    ) -> Self {
128        let mut params = DEFAULT_PARAMS_ANDERSON;
129        params.log = log.unwrap_or(params.log);
130        params.max_iter = max_iter.unwrap_or(params.max_iter);
131        params.tol = tol.unwrap_or(params.tol);
132        params.damping_coefficient = damping_coefficient.unwrap_or(params.damping_coefficient);
133        params.mmax = mmax.unwrap_or(params.mmax);
134        self.algorithms.push(DFTAlgorithm::AndersonMixing(params));
135        self
136    }
137
138    pub fn newton(
139        mut self,
140        log: Option<bool>,
141        max_iter: Option<usize>,
142        max_iter_gmres: Option<usize>,
143        tol: Option<f64>,
144    ) -> Self {
145        let mut params = DEFAULT_PARAMS_NEWTON;
146        params.log = log.unwrap_or(params.log);
147        params.max_iter = max_iter.unwrap_or(params.max_iter);
148        params.max_iter_gmres = max_iter_gmres.unwrap_or(params.max_iter_gmres);
149        params.tol = tol.unwrap_or(params.tol);
150        self.algorithms.push(DFTAlgorithm::Newton(params));
151        self
152    }
153}
154
155/// A log that stores the residuals and execution time of DFT solvers.
156#[derive(Clone)]
157pub struct DFTSolverLog {
158    verbosity: Verbosity,
159    start_time: Instant,
160    residual: Vec<f64>,
161    time: Vec<Duration>,
162    solver: Vec<&'static str>,
163}
164
165impl DFTSolverLog {
166    pub(crate) fn new(verbosity: Verbosity) -> Self {
167        log_iter!(
168            verbosity,
169            "solver                 | iter |    time    | residual "
170        );
171        Self {
172            verbosity,
173            start_time: Instant::now(),
174            residual: Vec::new(),
175            time: Vec::new(),
176            solver: Vec::new(),
177        }
178    }
179
180    fn add_residual(&mut self, solver: &'static str, iteration: usize, residual: f64) {
181        if iteration == 0 {
182            log_iter!(self.verbosity, "{:-<59}", "");
183        }
184        self.solver.push(solver);
185        self.residual.push(residual);
186        let time = self.start_time.elapsed();
187        self.time.push(self.start_time.elapsed());
188        log_iter!(
189            self.verbosity,
190            "{:22} | {:>4} | {:7.3} | {:.6e}",
191            solver,
192            iteration,
193            time.as_secs_f64() * SECOND,
194            residual,
195        );
196    }
197
198    pub fn residual(&self) -> ArrayView1<'_, f64> {
199        (&self.residual).into()
200    }
201
202    pub fn time(&self) -> Time<Array1<f64>> {
203        self.time.iter().map(|t| t.as_secs_f64() * SECOND).collect()
204    }
205
206    pub fn solver(&self) -> &[&'static str] {
207        &self.solver
208    }
209}
210
211impl<D: Dimension, F: HelmholtzEnergyFunctional> DFTProfile<D, F>
212where
213    D::Larger: Dimension<Smaller = D>,
214    <D::Larger as Dimension>::Larger: Dimension<Smaller = D::Larger>,
215{
216    pub(crate) fn call_solver(
217        &mut self,
218        rho: &mut Array<f64, D::Larger>,
219        rho_bulk: &mut Array1<f64>,
220        solver: &DFTSolver,
221        debug: bool,
222    ) -> FeosResult<()> {
223        let mut converged = false;
224        let mut iterations = 0;
225        let mut log = DFTSolverLog::new(solver.verbosity);
226        for algorithm in &solver.algorithms {
227            let (conv, iter) = match algorithm {
228                DFTAlgorithm::PicardIteration(picard) => {
229                    self.solve_picard(*picard, rho, rho_bulk, &mut log)
230                }
231                DFTAlgorithm::AndersonMixing(anderson) => {
232                    self.solve_anderson(*anderson, rho, rho_bulk, &mut log)
233                }
234                DFTAlgorithm::Newton(newton) => self.solve_newton(*newton, rho, rho_bulk, &mut log),
235            }?;
236            converged = conv;
237            iterations += iter;
238        }
239        self.solver_log = Some(log);
240        if converged {
241            log_result!(solver.verbosity, "DFT solved in {} iterations", iterations);
242        } else if debug {
243            log_result!(
244                solver.verbosity,
245                "DFT not converged in {} iterations",
246                iterations
247            );
248        } else {
249            return Err(FeosError::NotConverged(String::from("DFT")));
250        }
251        Ok(())
252    }
253
254    fn solve_picard(
255        &self,
256        picard: PicardIteration,
257        rho: &mut Array<f64, D::Larger>,
258        rho_bulk: &mut Array1<f64>,
259        log: &mut DFTSolverLog,
260    ) -> FeosResult<(bool, usize)> {
261        let solver = if picard.log {
262            "Picard iteration (log)"
263        } else {
264            "Picard iteration"
265        };
266
267        for k in 0..picard.max_iter {
268            // calculate residual
269            let (res, res_bulk, res_norm, _, _) =
270                self.euler_lagrange_equation(&*rho, &*rho_bulk, picard.log)?;
271            log.add_residual(solver, k, res_norm);
272
273            // check for convergence
274            if res_norm < picard.tol {
275                return Ok((true, k));
276            }
277
278            // apply line search or constant damping
279            let damping_coefficient = picard.damping_coefficient.map_or_else(
280                || self.line_search(rho, &res, rho_bulk, res_norm, picard.log),
281                Ok,
282            )?;
283
284            // update solution
285            if picard.log {
286                *rho *= &(&res * damping_coefficient).mapv(f64::exp);
287                *rho_bulk *= &(&res_bulk * damping_coefficient).mapv(f64::exp);
288            } else {
289                *rho += &(&res * damping_coefficient);
290                *rho_bulk += &(&res_bulk * damping_coefficient);
291            }
292        }
293        Ok((false, picard.max_iter))
294    }
295
296    fn line_search(
297        &self,
298        rho: &Array<f64, D::Larger>,
299        delta_rho: &Array<f64, D::Larger>,
300        rho_bulk: &Array1<f64>,
301        res0: f64,
302        logarithm: bool,
303    ) -> FeosResult<f64> {
304        let mut alpha = 2.0;
305
306        // reduce step until a feasible solution is found
307        for _ in 0..8 {
308            alpha *= 0.5;
309
310            // calculate full step
311            let rho_new = if logarithm {
312                rho * (alpha * delta_rho).mapv(f64::exp)
313            } else {
314                rho + alpha * delta_rho
315            };
316            let Ok((_, _, res2, _, _)) =
317                self.euler_lagrange_equation(&rho_new, rho_bulk, logarithm)
318            else {
319                continue;
320            };
321            if res2 > res0 {
322                continue;
323            }
324
325            // calculate intermediate step
326            let rho_new = if logarithm {
327                rho * (0.5 * alpha * delta_rho).mapv(f64::exp)
328            } else {
329                rho + 0.5 * alpha * delta_rho
330            };
331            let Ok((_, _, res1, _, _)) =
332                self.euler_lagrange_equation(&rho_new, rho_bulk, logarithm)
333            else {
334                continue;
335            };
336
337            // estimate minimum
338            let mut alpha_opt = if res2 - 2.0 * res1 + res0 != 0.0 {
339                alpha * 0.25 * (res2 - 4.0 * res1 + 3.0 * res0) / (res2 - 2.0 * res1 + res0)
340            } else {
341                continue;
342            };
343
344            // prohibit negative steps
345            if alpha_opt <= 0.0 {
346                alpha_opt = if res1 < res2 { 0.5 * alpha } else { alpha };
347            }
348
349            // prohibit too large steps
350            if alpha_opt > alpha {
351                alpha_opt = alpha;
352            }
353            alpha = alpha_opt;
354            break;
355        }
356        // log_iter!(verbosity, "    Line search      | {} | ", alpha);
357        Ok(alpha)
358    }
359
360    fn solve_anderson(
361        &self,
362        anderson: AndersonMixing,
363        rho: &mut Array<f64, D::Larger>,
364        rho_bulk: &mut Array1<f64>,
365        log: &mut DFTSolverLog,
366    ) -> FeosResult<(bool, usize)> {
367        let solver = if anderson.log {
368            "Anderson mixing (log)"
369        } else {
370            "Anderson mixing"
371        };
372
373        let mut resm = VecDeque::with_capacity(anderson.mmax);
374        let mut rhom = VecDeque::with_capacity(anderson.mmax);
375        let mut r;
376        let mut alpha;
377
378        for k in 0..anderson.max_iter {
379            // drop old values
380            if resm.len() == anderson.mmax {
381                resm.pop_front();
382                rhom.pop_front();
383            }
384            let m = resm.len() + 1;
385
386            // calculate residual
387            let (res, res_bulk, res_norm, _, _) =
388                self.euler_lagrange_equation(&*rho, &*rho_bulk, anderson.log)?;
389            log.add_residual(solver, k, res_norm);
390
391            // check for convergence
392            if res_norm < anderson.tol {
393                return Ok((true, k));
394            }
395
396            // save residual and x value
397            resm.push_back((res, res_bulk, res_norm));
398            if anderson.log {
399                rhom.push_back((rho.mapv(f64::ln), rho_bulk.mapv(f64::ln)));
400            } else {
401                rhom.push_back((rho.clone(), rho_bulk.clone()));
402            }
403
404            // calculate alpha
405            r = DMatrix::from_fn(m + 1, m + 1, |i, j| match (i == m, j == m) {
406                (false, false) => {
407                    let (resi, resi_bulk, _) = &resm[i];
408                    let (resj, resj_bulk, _) = &resm[j];
409                    (resi * resj).sum() + (resi_bulk * resj_bulk).sum()
410                }
411                (true, true) => 0.0,
412                _ => 1.0,
413            });
414            alpha = DVector::zeros(m + 1);
415            alpha[m] = 1.0;
416            let alpha = r.lu().solve(&alpha);
417            let alpha = alpha.ok_or(FeosError::Error("alpha matrix is not invertible".into()))?;
418
419            // update solution
420            rho.fill(0.0);
421            rho_bulk.fill(0.0);
422            for i in 0..m {
423                let (rhoi, rhoi_bulk) = &rhom[i];
424                let (resi, resi_bulk, _) = &resm[i];
425                *rho += &(alpha[i] * (rhoi + &(anderson.damping_coefficient * resi)));
426                *rho_bulk +=
427                    &(alpha[i] * (rhoi_bulk + &(anderson.damping_coefficient * resi_bulk)));
428            }
429            if anderson.log {
430                rho.mapv_inplace(f64::exp);
431                rho_bulk.mapv_inplace(f64::exp);
432            } else {
433                rho.mapv_inplace(f64::abs);
434                rho_bulk.mapv_inplace(f64::abs);
435            }
436        }
437        Ok((false, anderson.max_iter))
438    }
439
440    fn solve_newton(
441        &self,
442        newton: Newton,
443        rho: &mut Array<f64, D::Larger>,
444        rho_bulk: &mut Array1<f64>,
445        log: &mut DFTSolverLog,
446    ) -> FeosResult<(bool, usize)> {
447        let solver = if newton.log { "Newton (log)" } else { "Newton" };
448        for k in 0..newton.max_iter {
449            // calculate initial residual
450            let (res, _, res_norm, exp_dfdrho, rho_p) =
451                self.euler_lagrange_equation(rho, rho_bulk, newton.log)?;
452            log.add_residual(solver, k, res_norm);
453
454            // check convergence
455            if res_norm < newton.tol {
456                return Ok((true, k));
457            }
458
459            // calculate second partial derivatives once
460            let second_partial_derivatives = self.second_partial_derivatives(rho)?;
461
462            // define rhs function
463            let rhs = |delta_rho: &_| {
464                let mut delta_functional_derivative =
465                    self.delta_functional_derivative(delta_rho, &second_partial_derivatives);
466                delta_functional_derivative
467                    .outer_iter_mut()
468                    .zip(self.bulk.eos.m().iter())
469                    .for_each(|(mut q, &m)| q /= m);
470                let delta_i = self.delta_bond_integrals(&exp_dfdrho, &delta_functional_derivative);
471                let rho = if newton.log { &*rho } else { &rho_p };
472                delta_rho + (delta_functional_derivative - delta_i) * rho
473            };
474
475            // update solution
476            let lhs = if newton.log { &*rho * res } else { res };
477            *rho += &Self::gmres(rhs, &lhs, newton.max_iter_gmres, newton.tol * 1e-2, log)?;
478            rho.mapv_inplace(f64::abs);
479            rho_bulk.mapv_inplace(f64::abs);
480        }
481
482        Ok((false, newton.max_iter))
483    }
484
485    pub(crate) fn gmres<R>(
486        rhs: R,
487        r0: &Array<f64, D::Larger>,
488        max_iter: usize,
489        tol: f64,
490        log: &mut DFTSolverLog,
491    ) -> FeosResult<Array<f64, D::Larger>>
492    where
493        R: Fn(&Array<f64, D::Larger>) -> Array<f64, D::Larger>,
494    {
495        // allocate vectors and arrays
496        let mut v = Vec::with_capacity(max_iter);
497        let mut h: Array2<f64> = Array::zeros([max_iter + 1; 2]);
498        let mut c: Array1<f64> = Array::zeros(max_iter + 1);
499        let mut s: Array1<f64> = Array::zeros(max_iter + 1);
500        let mut gamma: Array1<f64> = Array::zeros(max_iter + 1);
501
502        gamma[0] = (r0 * r0).sum().sqrt();
503        v.push(r0 / gamma[0]);
504        log.add_residual("GMRES", 0, gamma[0]);
505
506        let mut iter = 0;
507        for j in 0..max_iter {
508            // calculate q=Av_j
509            let mut q = rhs(&v[j]);
510
511            // calculate h_ij
512            v.iter()
513                .enumerate()
514                .for_each(|(i, v_i)| h[(i, j)] = (v_i * &q).sum());
515
516            // calculate w_j (stored in q)
517            v.iter()
518                .enumerate()
519                .for_each(|(i, v_i)| q -= &(h[(i, j)] * v_i));
520            h[(j + 1, j)] = (&q * &q).sum().sqrt();
521
522            // update h_ij and h_i+1j
523            if j > 0 {
524                for i in 0..=j - 1 {
525                    let temp = c[i + 1] * h[(i, j)] + s[i + 1] * h[(i + 1, j)];
526                    h[(i + 1, j)] = -s[i + 1] * h[(i, j)] + c[i + 1] * h[(i + 1, j)];
527                    h[(i, j)] = temp;
528                }
529            }
530
531            // update auxiliary variables
532            let beta = (h[(j, j)] * h[(j, j)] + h[(j + 1, j)] * h[(j + 1, j)]).sqrt();
533            s[j + 1] = h[(j + 1, j)] / beta;
534            c[j + 1] = h[(j, j)] / beta;
535            h[(j, j)] = beta;
536            gamma[j + 1] = -s[j + 1] * gamma[j];
537            gamma[j] *= c[j + 1];
538
539            // check for convergence
540            log.add_residual("GMRES", j + 1, gamma[j + 1].abs());
541            if gamma[j + 1].abs() >= tol && j + 1 < max_iter {
542                v.push(q / h[(j + 1, j)]);
543                iter += 1;
544            } else {
545                break;
546            }
547        }
548        // calculate solution vector
549        let mut x = Array::zeros(r0.raw_dim());
550        let mut y = Array::zeros(iter + 1);
551        for i in (0..=iter).rev() {
552            y[i] = (gamma[i] - (i + 1..=iter).map(|k| h[(i, k)] * y[k]).sum::<f64>()) / h[(i, i)];
553        }
554        v.iter().zip(y).for_each(|(v, y)| x += &(y * v));
555        Ok(x)
556    }
557
558    pub(crate) fn second_partial_derivatives(
559        &self,
560        density: &Array<f64, D::Larger>,
561    ) -> FeosResult<Vec<Array<f64, <D::Larger as Dimension>::Larger>>> {
562        let temperature = self.temperature.to_reduced();
563        let contributions = self.bulk.eos.contributions();
564        let weighted_densities = self.convolver.weighted_densities(density);
565        let mut second_partial_derivatives = Vec::new();
566        for (c, wd) in contributions.into_iter().zip(&weighted_densities) {
567            let nwd = wd.shape()[0];
568            let ngrid = wd.len() / nwd;
569            let mut phi = Array::zeros(density.raw_dim().remove_axis(Axis(0)));
570            let mut pd = Array::zeros(wd.raw_dim());
571            let dim = wd.shape();
572            let dim: Vec<_> = std::iter::once(&nwd).chain(dim).cloned().collect();
573            let mut pd2 = Array::zeros(dim).into_dimensionality().unwrap();
574            c.second_partial_derivatives(
575                temperature,
576                wd.view().into_shape_with_order((nwd, ngrid)).unwrap(),
577                phi.view_mut().into_shape_with_order(ngrid).unwrap(),
578                pd.view_mut().into_shape_with_order((nwd, ngrid)).unwrap(),
579                pd2.view_mut()
580                    .into_shape_with_order((nwd, nwd, ngrid))
581                    .unwrap(),
582            )?;
583            second_partial_derivatives.push(pd2);
584        }
585        Ok(second_partial_derivatives)
586    }
587
588    pub(crate) fn delta_functional_derivative(
589        &self,
590        delta_density: &Array<f64, D::Larger>,
591        second_partial_derivatives: &[Array<f64, <D::Larger as Dimension>::Larger>],
592    ) -> Array<f64, D::Larger> {
593        let delta_weighted_densities = self.convolver.weighted_densities(delta_density);
594        let delta_partial_derivatives: Vec<_> = second_partial_derivatives
595            .iter()
596            .zip(delta_weighted_densities)
597            .map(|(pd2, wd)| {
598                let mut delta_partial_derivatives =
599                    Array::zeros(pd2.raw_dim().remove_axis(Axis(0)));
600                let n = wd.shape()[0];
601                for i in 0..n {
602                    for j in 0..n {
603                        delta_partial_derivatives
604                            .index_axis_mut(Axis(0), i)
605                            .add_assign(
606                                &(&pd2.index_axis(Axis(0), i).index_axis(Axis(0), j)
607                                    * &wd.index_axis(Axis(0), j)),
608                            );
609                    }
610                }
611                delta_partial_derivatives
612            })
613            .collect();
614        self.convolver
615            .functional_derivative(&delta_partial_derivatives)
616    }
617
618    pub(crate) fn delta_bond_integrals(
619        &self,
620        exponential: &Array<f64, D::Larger>,
621        delta_functional_derivative: &Array<f64, D::Larger>,
622    ) -> Array<f64, D::Larger> {
623        let temperature = self.temperature.to_reduced();
624
625        // calculate weight functions
626        let bond_lengths = self.bulk.eos.bond_lengths(temperature).into_edge_type();
627        let mut bond_weight_functions = bond_lengths.map(
628            |_, _| (),
629            |_, &l| WeightFunction::new_scaled(dvector![l], WeightFunctionShape::Delta),
630        );
631        for n in bond_lengths.node_indices() {
632            for e in bond_lengths.edges(n) {
633                bond_weight_functions.add_edge(
634                    e.target(),
635                    e.source(),
636                    WeightFunction::new_scaled(dvector![*e.weight()], WeightFunctionShape::Delta),
637                );
638            }
639        }
640
641        let mut i_graph: Graph<_, Option<Array<f64, D>>, Directed> =
642            bond_weight_functions.map(|_, _| (), |_, _| None);
643        let mut delta_i_graph: Graph<_, Option<Array<f64, D>>, Directed> =
644            bond_weight_functions.map(|_, _| (), |_, _| None);
645
646        let bonds = i_graph.edge_count();
647        let mut calc = 0;
648
649        // go through the whole graph until every bond has been calculated
650        while calc < bonds {
651            let mut edge_id = None;
652            let mut i1 = None;
653            let mut delta_i1 = None;
654
655            // find the first bond that can be calculated
656            'nodes: for node in i_graph.node_indices() {
657                for edge in i_graph.edges(node) {
658                    // skip already calculated bonds
659                    if edge.weight().is_some() {
660                        continue;
661                    }
662
663                    // if all bonds from the neighboring segment are calculated calculate the bond
664                    let edges = i_graph
665                        .edges(edge.target())
666                        .filter(|e| e.target().index() != node.index());
667                    let delta_edges = delta_i_graph
668                        .edges(edge.target())
669                        .filter(|e| e.target().index() != node.index());
670                    if edges.clone().all(|e| e.weight().is_some()) {
671                        edge_id = Some(edge.id());
672                        let i0 = edges.fold(
673                            exponential
674                                .index_axis(Axis(0), edge.target().index())
675                                .to_owned(),
676                            |acc: Array<f64, _>, e| acc * e.weight().as_ref().unwrap(),
677                        );
678                        let delta_i0 = delta_edges.fold(
679                            -&delta_functional_derivative
680                                .index_axis(Axis(0), edge.target().index()),
681                            |acc: Array<f64, _>, delta_e| acc + delta_e.weight().as_ref().unwrap(),
682                        ) * &i0;
683                        i1 = Some(
684                            self.convolver
685                                .convolve(i0, &bond_weight_functions[edge.id()]),
686                        );
687                        delta_i1 = Some(
688                            (self
689                                .convolver
690                                .convolve(delta_i0, &bond_weight_functions[edge.id()])
691                                / i1.as_ref().unwrap())
692                            .mapv(|x| if x.is_finite() { x } else { 0.0 }),
693                        );
694                        break 'nodes;
695                    }
696                }
697            }
698            if let Some(edge_id) = edge_id {
699                i_graph[edge_id] = i1;
700                delta_i_graph[edge_id] = delta_i1;
701                calc += 1;
702            } else {
703                panic!("Cycle in molecular structure detected!")
704            }
705        }
706
707        let mut delta_i = Array::zeros(exponential.raw_dim());
708        for node in delta_i_graph.node_indices() {
709            for edge in delta_i_graph.edges(node) {
710                delta_i
711                    .index_axis_mut(Axis(0), node.index())
712                    .add_assign(edge.weight().as_ref().unwrap());
713            }
714        }
715
716        delta_i
717    }
718}
719
720impl fmt::Display for DFTAlgorithm {
721    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
722        match self {
723            Self::PicardIteration(picard) => write!(f, "{picard:?}"),
724            Self::AndersonMixing(anderson) => write!(f, "{anderson:?}"),
725            Self::Newton(newton) => write!(f, "{newton:?}"),
726        }
727    }
728}
729
730impl fmt::Display for DFTSolver {
731    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
732        writeln!(f, "DFTSolver(")?;
733        for algorithm in &self.algorithms {
734            writeln!(f, "    {algorithm}")?;
735        }
736        writeln!(f, ")")
737    }
738}
739
740impl DFTSolver {
741    pub fn _repr_markdown_(&self) -> String {
742        let mut res = String::from("|solver|max_iter|tol|\n|-|-:|-:|");
743        for algorithm in &self.algorithms {
744            let (solver, max_iter, tol) = match algorithm {
745                DFTAlgorithm::PicardIteration(picard) => (
746                    format!(
747                        "Picard iteration ({}{})",
748                        if picard.log { "log, " } else { "" },
749                        match picard.damping_coefficient {
750                            None => "line search".into(),
751                            Some(damping_coefficient) =>
752                                format!("damping_coefficient={damping_coefficient}"),
753                        }
754                    ),
755                    picard.max_iter,
756                    picard.tol,
757                ),
758                DFTAlgorithm::AndersonMixing(anderson) => (
759                    format!(
760                        "Anderson mixing ({}damping_coefficient={}, mmax={})",
761                        if anderson.log { "log, " } else { "" },
762                        anderson.damping_coefficient,
763                        anderson.mmax
764                    ),
765                    anderson.max_iter,
766                    anderson.tol,
767                ),
768                DFTAlgorithm::Newton(newton) => (
769                    format!(
770                        "Newton ({}max_iter_gmres={})",
771                        if newton.log { "log, " } else { "" },
772                        newton.max_iter_gmres
773                    ),
774                    newton.max_iter,
775                    newton.tol,
776                ),
777            };
778            res += &format!("\n|{solver}|{max_iter}|{tol:e}|");
779        }
780        res
781    }
782}