te1d/teg/
mod.rs

1//! Simulates steady-state thermoelectric power generation.
2
3use std::fmt;
4use std::rc::Rc;
5
6use ndarray::{Array, Array2, array, s};
7
8use crate::calc::{ColVec, integrate_simpson, is_close};
9use crate::func::{RealValuedFunction};
10use crate::ode::{explicit_rk4, explicit_rk4_fvp};
11use crate::opt::{OptimizeResult, MinimizerKind, MinimizerParam, minimize, minimize_brent};
12use crate::tep::{Tep};
13use crate::bc::{BoundaryCondition, OptVarConverter, FullOptVar};
14use crate::simul::{SimulationErrorKind, convert_opt_error_to_simul_error};
15use crate::interp::{PiecewiseCubicPolynomialConstExt};
16
17pub mod prelude;
18
19
20/// Parameters for energy generation module simulation
21/// 
22/// # Tips
23/// * Setting `dx: length/22.0` and `grad_tol: 1e-07` is a good choice for high accuracy.
24/// * Setting `dx: length/10.0` and `grad_tol: 1e-06` is a good choice for fast computation.
25pub struct SimulationParam {
26    /// A suggested size of `x`-meshes to discretize the space in legs. Real size of `x`-meshes can be different.
27    pub dx: f64,
28    /// A suggested size of `T`-meshes to discretize the temperature intervals. Real size of `T`-meshes can be different.
29    pub dtemp: f64,
30    /// Minimum number of `x`-meshes. If `dx` is too large, `min_num_x_mesh` is used to prevent a coarse mesh.
31    pub min_num_x_mesh: usize,
32    /// Minimum number of `T`-meshes. If `dtemp` is too large, `min_num_temp_mesh` is used to prevent a coarse mesh.
33    pub min_num_temp_mesh: usize,
34    /// Maximum number of `x`-meshes. If `dx` is too small, `max_num_x_mesh` is used to prevent a excessively fine mesh.
35    pub max_num_x_mesh: usize,
36    /// Maximum number of `T`-meshes. If `dtemp` is too small, `max_num_temp_mesh` is used to prevent a excessively fine mesh.
37    pub max_num_temp_mesh: usize,
38    /// The kind of minimizer that is used for matching the boundary conditions
39    pub minimizer_kind: MinimizerKind,
40    /// The parameters of the minimizer
41    pub minimizer_param: MinimizerParam,
42    /// Relative tolerance in temperature (K) matching
43    pub rel_tol_temp: f64,
44    /// Absolute tolerance in temperature (K) matching
45    pub abs_tol_temp: f64,
46    /// Relative tolerance in heat rate (W) matching
47    pub rel_tol_heat_rate: f64,
48    /// Absolute tolerance in heat rate (W) matching
49    pub abs_tol_heat_rate: f64,
50    /// Maximum number of iterations to perform Brent-Dekker minimization
51    pub max_iter_brent: usize,
52    /// Absolute tolerance in electrical current (A), used in Brent-Dekker minimization algorithm
53    pub abs_tol_elec_cur_brent: f64,
54}
55
56impl SimulationParam {
57    /// Returns the default parameters.
58    /// * **Modify** `dx` and `dtemp` according to the length of the leg and the size of temperature interval.
59    pub fn default() -> Self {
60        SimulationParam {
61            dx: 1e-4,      // (m) (=0.1 mm)
62            dtemp: 10.0,   // (K)
63            min_num_x_mesh: 11,
64            min_num_temp_mesh: 11,
65            max_num_x_mesh: 101,
66            max_num_temp_mesh: 101,
67            minimizer_kind: MinimizerKind::Bfgs,
68            minimizer_param: MinimizerParam::default(MinimizerKind::Bfgs),
69            rel_tol_temp: 1e-4,
70            abs_tol_temp: 1e-2,
71            rel_tol_heat_rate: 1e-4,
72            abs_tol_heat_rate: 1e-4,
73            max_iter_brent: 40,
74            abs_tol_elec_cur_brent: 1e-6,
75        }
76    }
77}
78
79
80/// Results of leg simulation for energy generation
81pub struct LegSimulationResult {
82    /// The temperature distribution inside a leg
83    pub temp: Rc<dyn RealValuedFunction>,
84    /// The heat flux distribution inside a leg
85    pub heat_flux: Rc<dyn RealValuedFunction>,
86    /// Electrical Current (A)
87    pub elec_cur: f64,
88    /// Voltage (V) induced from the Seebeck effect
89    pub voltage: f64,
90    /// Resistance (Ohm) inside a leg
91    pub internal_resistance: f64,
92    /// Temperature (K) at the left end
93    pub templ: f64,
94    /// Temperature (K) at the right end
95    pub tempr: f64,
96    /// Heat rate (W) at the left end
97    pub heat_ratel: f64,
98    /// Heat rate (W) at the right end
99    pub heat_rater: f64,
100    /// Thermoelectric power (W)
101    pub power: f64,
102    /// Energy conversion efficiency (1)
103    pub efficiency: f64,
104    /// A boolean flag indicating if the simulation exited successfully
105    pub success: bool,
106    /// Error from the simulation, if nay
107    pub error: Option<SimulationErrorKind>,
108    /// Number of iterations in optimization process
109    pub num_iter: usize,
110}
111
112impl fmt::Display for LegSimulationResult {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        write!(f, "LegSimulationResult {{ success: {}, elec_cur: {}, voltage: {}, internal_resistance: {}, templ: {}, tempr: {}, heat_ratel: {}, heat_rater: {}, power: {}, efficiency: {}, .. }}",
115            self.success, self.elec_cur, self.voltage, self.internal_resistance, self.templ, self.tempr, self.heat_ratel, self.heat_rater, self.power, self.efficiency)
116    }
117}
118
119impl LegSimulationResult {
120    /// Returns the sum of internal and external load resistances (Ohm)
121    #[inline]
122    pub fn total_resistance(&self) -> f64 {
123        self.voltage / self.elec_cur
124    }
125
126    /// Returns the external load resistance (Ohm); if the `elec_cur==0`, then `NAN` is returned.
127    #[inline]
128    pub fn load_resistance(&self) -> f64 {
129        self.voltage / self.elec_cur - self.internal_resistance
130    }
131
132    // Returns the external-to-internal reistance ratio (`=load_resistance/internal_reistance`)
133    #[inline]
134    pub fn load_ratio(&self) -> f64 {
135        self.voltage / (self.elec_cur * self.internal_resistance) - 1.0
136    }
137}
138
139
140/// Simulator of single-leg thermoelectric generators
141/// 
142/// # Examples
143/// The following is a simple example simulating a constant property model under a fixed electrical current.
144/// ```
145/// use std::rc::Rc;
146/// use ndarray::prelude::*;
147/// use te1d::teg::prelude::*;
148/// use te1d::func::ConstantFunction;
149///
150/// // set tep and leg parameters
151/// let alpha0: f64 = 1.2;
152/// let rho0: f64 = 3.0;
153/// let kappa0: f64 = 1.5;
154/// let tep = Tep::new(Rc::new(ConstantFunction::new(alpha0)),
155///                    Rc::new(ConstantFunction::new(rho0)),
156///                    Rc::new(ConstantFunction::new(kappa0)));
157/// let length = 0.8;
158/// let area = 0.3;
159/// 
160/// // set simulation parameters
161/// let minimizer_kind = MinimizerKind::Bfgs;
162/// let minimizer_param = MinimizerParam {
163///     grad_tol: 1e-05,
164///     a_max: 2.0,
165///     max_iter: 200,
166///     ..MinimizerParam::default(minimizer_kind)
167/// };
168/// let simul_param = SimulationParam {
169///     dx: length/10.0,
170///     minimizer_kind,
171///     minimizer_param,
172///     ..SimulationParam::default()
173/// };
174/// 
175/// // create a leg
176/// let leg = SingleLeg::new(&tep, length, area, simul_param);
177/// let elec_cur: f64 = 1.3;
178/// let j: f64 = elec_cur / area;
179/// let hot_temp: f64 = 600.0;
180/// let cold_temp: f64 = 300.0;
181/// let left_bc = FixedTempBc::new(hot_temp);
182/// let right_bc = FixedTempBc::new(cold_temp);
183/// 
184/// // simulate
185/// let simul_result = leg.simulate_fixed_elec_cur(elec_cur, &left_bc, &right_bc, hot_temp, cold_temp);
186/// assert!(simul_result.success);
187/// 
188/// // check the solution
189/// let x_mesh: ColVec = Array::linspace(0.0, length, 31);
190/// let temp_numerical: ColVec = simul_result.temp.ats_incr(&x_mesh);
191/// let heat_flux_numerical: ColVec = simul_result.heat_flux.ats_incr(&x_mesh);
192/// let temp_exact: ColVec = hot_temp + (cold_temp-hot_temp)*&x_mesh/length + j.powi(2)*0.5*rho0/kappa0*&x_mesh*(length-&x_mesh);
193/// let dtempdx_exact: ColVec = (cold_temp-hot_temp)/length + j.powi(2)*0.5*rho0/kappa0*(length-2.0*&x_mesh);
194/// let heat_flux_exact: ColVec = alpha0*&temp_exact*j - kappa0*&dtempdx_exact;
195/// 
196/// assert!(all_close(&temp_numerical, &temp_exact, 1e-05, 0.0));
197/// assert!(all_close(&heat_flux_numerical, &heat_flux_exact, 1e-05, 0.0));
198/// ```
199pub struct SingleLeg {
200    /// Thermoelectric material properties
201    pub tep: Tep,
202    /// Length (m) of a leg
203    pub length: f64,
204    /// Cross-sectional area (m^2) of a leg
205    pub area: f64,
206    /// Simulation parameters
207    pub simulation_param: SimulationParam,
208    /// The mesh points discretizing the sptial coordinate inside the leg. Must be equidistant.
209    pub xs: ColVec,
210    /// The left-half mesh points.
211    xs_left: ColVec,
212    /// The right-half mesh points.
213    xs_right: ColVec,
214}
215
216impl SingleLeg {
217    /// Create a new simulator
218    /// 
219    /// # Panics
220    /// * when `length` is not positive.
221    /// * when `area` is not positive.
222    pub fn new(tep: &Tep, length: f64, area: f64, simulation_param: SimulationParam) -> Self {
223        if length <= 0.0 { panic!("`length` must be a positive number!"); }
224        if area <= 0.0 { panic!("`area` must be a positive number!"); }
225
226        // generate mesh points
227        let num_x_mesh: usize = get_num_mesh(length, simulation_param.dx, simulation_param.min_num_x_mesh, simulation_param.max_num_x_mesh);
228        let xs: ColVec = Array::linspace(0.0, length, num_x_mesh);
229        let half_num_x_mesh: usize = (num_x_mesh+1)/2;  // `num_x_mesh` must be odd
230        let xs_left: ColVec = xs.slice(s![0..half_num_x_mesh]).to_owned();
231        let xs_right: ColVec = xs.slice(s![half_num_x_mesh-1..num_x_mesh]).to_owned();
232
233        // generate a thermoelectric leg
234        SingleLeg { tep: tep.clone(), length, area, simulation_param, xs, xs_left, xs_right }
235    }
236
237    /// Find the steady state under the given boundary condition and electrical current (A).
238    /// * You need to provide hints for electrical current (A), left-end temperature (K) and right-end temperature (K).
239    /// * If this does not converge, use [`SingleLeg::simulate_fixed_elec_cur_with_manual_init()`] to provide initial values manually.
240    pub fn simulate_fixed_elec_cur(&self, fixed_elec_cur: f64, left_bc: &(dyn BoundaryCondition), right_bc: &(dyn BoundaryCondition), templ_hint: f64, tempr_hint: f64) -> LegSimulationResult {
241        let fixed_elec_cur_density: f64 = fixed_elec_cur / self.area;
242        let heat_rate_hint: f64 = self.estimate_heat_flux(templ_hint, tempr_hint, fixed_elec_cur_density) * self.area;
243        let init_var: FullOptVar = FullOptVar { templ: templ_hint, heat_ratel: heat_rate_hint, tempr: tempr_hint, heat_rater: heat_rate_hint, elec_cur: fixed_elec_cur };
244
245        self.simulate_fixed_elec_cur_with_manual_init(fixed_elec_cur, left_bc, right_bc, &init_var)
246    }
247
248    /// Find the steady state under the given boundary condition and electrical current (A).
249    /// * You need to provide initial values for optimization by completing [`FullOptVar`](crate::bc::FullOptVar)
250    pub fn simulate_fixed_elec_cur_with_manual_init(&self, fixed_elec_cur: f64, left_bc: &(dyn BoundaryCondition), right_bc: &(dyn BoundaryCondition), init_var: &FullOptVar ) -> LegSimulationResult {
251        let converter: OptVarConverter = OptVarConverter::new(left_bc, right_bc, Some(fixed_elec_cur), self.area);
252        let n_half_cols: usize = self.xs_left.len();
253
254        let cost_f = |var: &ColVec| -> f64 {
255            let full_var: FullOptVar = converter.to_full_var(left_bc, right_bc, var);
256            let elec_cur_density: f64 = full_var.elec_cur / self.area;
257            let ivp_ys = self.solve_ivp(elec_cur_density, full_var.templ, full_var.heat_ratel/self.area, &self.xs_left);
258            let fvp_ys = self.solve_fvp(elec_cur_density, full_var.tempr, full_var.heat_rater/self.area, &self.xs_right);
259
260            let ivp_temp_mid = ivp_ys[[0, n_half_cols-1]];
261            let ivp_heat_rate_mid = ivp_ys[[1, n_half_cols-1]] * self.area;
262            let fvp_temp_mid = fvp_ys[[0, 0]];
263            let fvp_heat_rate_mid = fvp_ys[[1, 0]] * self.area;
264
265            let mut cost = (ivp_temp_mid - fvp_temp_mid).powi(2) + (ivp_heat_rate_mid - fvp_heat_rate_mid).powi(2);
266            cost *= 0.5;
267
268            cost
269        };
270
271        self.minimize_cost_by_shooting(&cost_f, left_bc, right_bc, &converter, init_var)
272    }
273
274    /// Find the steady state under the given boundary condition and external resistance (Ohm).
275    /// * You need to provide hints for electrical current (A), left-end temperature (K) and right-end temperature (K).
276    /// * If this does not converge, use [`SingleLeg::simulate_fixed_load_resistance_with_manual_init()`] to provide initial values manually.
277    pub fn simulate_fixed_load_resistance(&self, fixed_load_resistance: f64, left_bc: &(dyn BoundaryCondition), right_bc: &(dyn BoundaryCondition), elec_cur_hint: f64, templ_hint: f64, tempr_hint: f64) -> LegSimulationResult {
278        let elec_cur_density_hint: f64 = elec_cur_hint / self.area;
279        let heat_rate_hint: f64 = self.estimate_heat_flux(templ_hint, tempr_hint, elec_cur_density_hint) * self.area;
280        let init_var: FullOptVar = FullOptVar { templ: templ_hint, heat_ratel: heat_rate_hint, tempr: tempr_hint, heat_rater: heat_rate_hint, elec_cur: elec_cur_hint };
281
282        self.simulate_fixed_load_resistance_with_manual_init(fixed_load_resistance, left_bc, right_bc, &init_var)
283    }
284
285    /// Find the steady state under the given boundary condition and external resistance (Ohm).
286    /// * You need to provide initial values for optimization by completing [`FullOptVar`](crate::bc::FullOptVar)
287    pub fn simulate_fixed_load_resistance_with_manual_init(&self, fixed_load_resistance: f64, left_bc: &(dyn BoundaryCondition), right_bc: &(dyn BoundaryCondition), init_var: &FullOptVar) -> LegSimulationResult {
288        let converter: OptVarConverter = OptVarConverter::new(left_bc, right_bc, None, self.area);
289        let n_half_cols: usize = self.xs_left.len();
290        let n_cols: usize = self.xs.len();
291
292        let cost_f = |var: &ColVec| -> f64 {
293            let full_var = converter.to_full_var(left_bc, right_bc, var);    
294            let elec_cur_density: f64 = full_var.elec_cur / self.area;
295            let ivp_ys = self.solve_ivp(elec_cur_density, full_var.templ, full_var.heat_ratel / self.area, &self.xs);
296            let fvp_ys = self.solve_fvp(elec_cur_density, full_var.tempr, full_var.heat_rater / self.area, &self.xs);
297            let ivp_temp_mid = ivp_ys[[0, n_half_cols-1]];
298            let ivp_heat_rate_mid = ivp_ys[[1, n_half_cols-1]] * self.area;
299            let fvp_temp_mid = fvp_ys[[0, n_half_cols-1]];
300            let fvp_heat_rate_mid = fvp_ys[[1, n_half_cols-1]] * self.area;
301            let ys = (1.0-&self.xs/self.length) * &ivp_ys + &self.xs/self.length * &fvp_ys;  // this enforces the correct BCs
302
303            let voltage = self.compute_voltage(ys[[0, 0]], ys[[0, n_cols-1]]);
304            let internal_resistance = self.compute_internal_resistance(&ys.row(0).to_owned());
305            let elec_cur = full_var.elec_cur;
306
307            let mut cost = (ivp_temp_mid - fvp_temp_mid).powi(2) + (ivp_heat_rate_mid - fvp_heat_rate_mid).powi(2);
308            cost *= 0.5;
309            cost += (voltage - elec_cur*(internal_resistance + fixed_load_resistance)).powi(2) * 1e3;
310
311            cost
312        };
313
314        self.minimize_cost_by_shooting(&cost_f, left_bc, right_bc, &converter, init_var)
315    }
316
317    /// Find the steady state under the given boundary condition and external-to-internal load ratio (1).
318    /// * You need to provide hints for electrical current (A), left-end temperature (K) and right-end temperature (K).
319    /// * If this does not converge, use [`SingleLeg::simulate_fixed_load_ratio_with_manual_init()`] to provide initial values manually.
320    pub fn simulate_fixed_load_ratio(&self, fixed_load_ratio: f64, left_bc: &(dyn BoundaryCondition), right_bc: &(dyn BoundaryCondition), elec_cur_hint: f64, templ_hint: f64, tempr_hint: f64) -> LegSimulationResult {
321        let elec_cur_density_hint: f64 = elec_cur_hint / self.area;
322        let heat_rate_hint: f64 = self.estimate_heat_flux(templ_hint, tempr_hint, elec_cur_density_hint) * self.area;
323        let init_var: FullOptVar = FullOptVar { templ: templ_hint, heat_ratel: heat_rate_hint, tempr: tempr_hint, heat_rater: heat_rate_hint, elec_cur: elec_cur_hint };
324
325        self.simulate_fixed_load_ratio_with_manual_init(fixed_load_ratio, left_bc, right_bc, &init_var)
326    }
327
328    /// Find the steady state under the given boundary condition and external-to-internal load ratio (1).
329    /// * You need to provide initial values for optimization by completing [`FullOptVar`](crate::bc::FullOptVar)
330    /// 
331    /// # Panics
332    /// * when `fixed_load_ratio` is infinite
333    /// * when `fixed_load_ratio` is negative
334    pub fn simulate_fixed_load_ratio_with_manual_init(&self, fixed_load_ratio: f64, left_bc: &(dyn BoundaryCondition), right_bc: &(dyn BoundaryCondition), init_var: &FullOptVar) -> LegSimulationResult {
335        if fixed_load_ratio.is_infinite() {
336            panic!("`fixed_load_ratio` must be a finite value!");
337        }
338        if fixed_load_ratio < 0.0 {
339            panic!("`fixed_load_ratio` must be a nonngeative value!");
340        }
341
342        let converter: OptVarConverter = OptVarConverter::new(left_bc, right_bc, None, self.area);
343        let n_half_cols: usize = self.xs_left.len();
344        let n_cols: usize = self.xs.len();
345
346        let cost_f = |var: &ColVec| -> f64 {
347            let full_var = converter.to_full_var(left_bc, right_bc, var);    
348            let elec_cur_density: f64 = full_var.elec_cur / self.area;
349            let ivp_ys = self.solve_ivp(elec_cur_density, full_var.templ, full_var.heat_ratel / self.area, &self.xs);
350            let fvp_ys = self.solve_fvp(elec_cur_density, full_var.tempr, full_var.heat_rater / self.area, &self.xs);
351            let ivp_temp_mid = ivp_ys[[0, n_half_cols-1]];
352            let ivp_heat_rate_mid = ivp_ys[[1, n_half_cols-1]] * self.area;
353            let fvp_temp_mid = fvp_ys[[0, n_half_cols-1]];
354            let fvp_heat_rate_mid = fvp_ys[[1, n_half_cols-1]] * self.area;
355            let ys = (1.0-&self.xs/self.length) * &ivp_ys + &self.xs/self.length * &fvp_ys;  // this enforces the correct BCs
356
357            let voltage = self.compute_voltage(ys[[0, 0]], ys[[0, n_cols-1]]);
358            let internal_resistance = self.compute_internal_resistance(&ys.row(0).to_owned());
359            let elec_cur = full_var.elec_cur;
360
361            let mut cost = (ivp_temp_mid - fvp_temp_mid).powi(2) + (ivp_heat_rate_mid - fvp_heat_rate_mid).powi(2);
362            cost *= 0.5;
363            cost += (voltage - elec_cur*(1.0 + fixed_load_ratio)*internal_resistance).powi(2) * 1e3;
364
365            //println!("var={}, cost={}", var, cost);
366
367            cost
368        };
369
370        self.minimize_cost_by_shooting(&cost_f, left_bc, right_bc, &converter, init_var)
371    }
372
373    /// Find the steady state minimizing the cost function. A shooting on electrical current, boundary temperature and boundary heat rate is performed.
374    /// * You need to provide initial values for optimization by completing [`FullOptVar`](crate::bc::FullOptVar)
375    fn minimize_cost_by_shooting(&self, cost_f: &(dyn Fn(&ColVec) -> f64), left_bc: &(dyn BoundaryCondition), right_bc: &(dyn BoundaryCondition), converter: &OptVarConverter, init_var: &FullOptVar) -> LegSimulationResult {
376        let var0: ColVec = converter.to_opt_var(init_var);
377        let n_half_cols: usize = self.xs_left.len();
378        let optimize_result: OptimizeResult = minimize(&cost_f, &var0, self.simulation_param.minimizer_kind, &self.simulation_param.minimizer_param);
379        let full_var: FullOptVar = converter.to_full_var(left_bc, right_bc, &optimize_result.x);
380
381        // Report the result
382        let mut success: bool = true;
383        let mut error: Option<SimulationErrorKind> = None;
384        // generate the solution by taking the average of two-side shooting solutions.
385        let elec_cur_density: f64 = full_var.elec_cur / self.area;
386        let ivp_ys = self.solve_ivp(elec_cur_density, full_var.templ, full_var.heat_ratel / self.area, &self.xs);
387        let fvp_ys = self.solve_fvp(elec_cur_density, full_var.tempr, full_var.heat_rater / self.area, &self.xs);
388        let ys = (1.0-&self.xs/self.length) * &ivp_ys + &self.xs/self.length * &fvp_ys;  // this enforces the correct BCs
389        // if the mid-point values are different, it is a failure
390        if !(is_close(ivp_ys[[0, n_half_cols]], fvp_ys[[0, n_half_cols]], self.simulation_param.rel_tol_temp, self.simulation_param.abs_tol_temp))
391           || !(is_close(ivp_ys[[1, n_half_cols]]/self.area, fvp_ys[[1, n_half_cols]]/self.area, self.simulation_param.rel_tol_heat_rate, self.simulation_param.abs_tol_heat_rate)) {
392            success = false;
393            error = Some(SimulationErrorKind::BcNotMatchError);
394        }
395        // if the optimization is incomplete, it is a failure
396        if !(optimize_result.success) {
397            success = false;
398            error = Some(convert_opt_error_to_simul_error(optimize_result.error.unwrap()));
399        }
400
401        let temps: ColVec = ys.row(0).to_owned();
402        let heat_fluxs: ColVec = ys.row(1).to_owned();
403        let heat_fluxl: f64 = full_var.heat_ratel/self.area;
404        let heat_fluxr: f64 = full_var.heat_rater/self.area;
405
406        LegSimulationResult {
407            temp: Rc::new(PiecewiseCubicPolynomialConstExt::new(&self.xs, &temps, full_var.templ, full_var.tempr)),
408            heat_flux: Rc::new(PiecewiseCubicPolynomialConstExt::new(&self.xs, &heat_fluxs, heat_fluxl, heat_fluxr)),
409            elec_cur: full_var.elec_cur,
410            voltage: self.compute_voltage(full_var.templ, full_var.tempr),
411            internal_resistance: self.compute_internal_resistance(&temps),
412            templ: full_var.templ,
413            tempr: full_var.tempr,
414            heat_ratel: full_var.heat_ratel,
415            heat_rater: full_var.heat_rater,
416            power: self.get_power(heat_fluxl, heat_fluxr),
417            efficiency: get_efficiency(heat_fluxl, heat_fluxr),
418            success,
419            error,
420            num_iter: optimize_result.num_iter,
421        }
422    }
423
424    /// Find the steady state under the given boundary condition and external-to-internal load ratio (1).
425    /// * You need to provide hints search range for electrical current (A), left-end temperature (K) and right-end temperature (K).
426    /// * If this does not converge, use [`SingleLeg::simulate_fixed_load_ratio_in_range_with_manual_init()`] to provide initial values manually.
427    pub fn simulate_fixed_load_ratio_in_range(&self, fixed_load_ratio: f64, left_bc: &(dyn BoundaryCondition), right_bc: &(dyn BoundaryCondition), min_elec_cur: f64, max_elec_cur: f64, templ_hint: f64, tempr_hint: f64) -> LegSimulationResult {
428        let elec_cur_hint: f64 = (min_elec_cur + max_elec_cur) * 0.5;
429        let elec_cur_density_hint: f64 = elec_cur_hint / self.area;
430        let heat_rate_hint: f64 = self.estimate_heat_flux(templ_hint, tempr_hint, elec_cur_density_hint) * self.area;
431        let init_var: FullOptVar = FullOptVar { templ: templ_hint, heat_ratel: heat_rate_hint, tempr: tempr_hint, heat_rater: heat_rate_hint, elec_cur: elec_cur_hint };
432
433        self.simulate_fixed_load_ratio_in_range_with_manual_init(fixed_load_ratio, left_bc, right_bc, min_elec_cur, max_elec_cur, &init_var)
434    }
435
436    /// Find the steady state under the given boundary condition and external-to-internal load ratio (1).
437    /// * You need to provide the search range for electrical current (A).
438    /// * You need to provide initial values for optimization by completing [`FullOptVar`](crate::bc::FullOptVar).
439    pub fn simulate_fixed_load_ratio_in_range_with_manual_init(&self, fixed_load_ratio: f64, left_bc: &(dyn BoundaryCondition), right_bc: &(dyn BoundaryCondition), min_elec_cur: f64, max_elec_cur: f64, init_var: &FullOptVar) -> LegSimulationResult {
440        let cost_f = |elec_cur: f64| -> f64 {
441            let result_bc = self.simulate_fixed_elec_cur_with_manual_init(elec_cur, left_bc, right_bc, init_var);
442
443            let cost: f64 = (result_bc.load_ratio() - fixed_load_ratio).powi(2);
444
445            cost
446        };
447        let optimize_result: OptimizeResult = minimize_brent(&cost_f, min_elec_cur, max_elec_cur, self.simulation_param.max_iter_brent, self.simulation_param.abs_tol_elec_cur_brent);
448        let elec_cur = optimize_result.x[0];
449        let result_bc = self.simulate_fixed_elec_cur_with_manual_init(elec_cur, left_bc, right_bc, init_var);
450
451        result_bc
452    }
453
454    /// Find the steady state where the power (W) is maximized.
455    /// * You need to provide hints for left-end temperature (K) and right-end temperature (K).
456    /// * If this does not converge, use [`SingleLeg::maximize_power_in_range()`] to provide a current range manually.
457    pub fn maximize_power_auto_range(&self, left_bc: &(dyn BoundaryCondition), right_bc: &(dyn BoundaryCondition), templ_hint: f64, tempr_hint: f64) -> LegSimulationResult {
458        let elec_cur_hint: f64 = self.estimate_elec_cur_for_max_power(templ_hint, tempr_hint);
459        let min_elec_cur: f64;
460        let max_elec_cur: f64;
461        if elec_cur_hint > 0.0 {
462            min_elec_cur = elec_cur_hint * 0.5;
463            max_elec_cur = elec_cur_hint * 1.5;
464        } else {
465            min_elec_cur = elec_cur_hint * 1.5;
466            max_elec_cur = elec_cur_hint * 0.5;
467        }
468
469        self.maximize_power_in_range(left_bc, right_bc, min_elec_cur, max_elec_cur, templ_hint, tempr_hint)
470    }
471
472    /// Find the steady state where the power (W) is maximized.
473    /// * You need to provide hints for search range for electrical current (A) and hints for left-end temperature (K) and right-end temperature (K).
474    /// * If this does not converge, use [`SingleLeg::maximize_power_in_range_with_manual_init()`] to provide initial values manually.
475    pub fn maximize_power_in_range(&self, left_bc: &(dyn BoundaryCondition), right_bc: &(dyn BoundaryCondition), min_elec_cur: f64, max_elec_cur: f64, templ_hint: f64, tempr_hint: f64) -> LegSimulationResult {
476        let elec_cur_hint: f64 = (min_elec_cur + max_elec_cur) * 0.5;
477        let elec_cur_density_hint: f64 = elec_cur_hint / self.area;
478        let heat_rate_hint: f64 = self.estimate_heat_flux(templ_hint, tempr_hint, elec_cur_density_hint) * self.area;
479        let init_var: FullOptVar = FullOptVar { templ: templ_hint, heat_ratel: heat_rate_hint, tempr: tempr_hint, heat_rater: heat_rate_hint, elec_cur: elec_cur_hint };
480
481        self.maximize_power_in_range_with_manual_init(left_bc, right_bc, min_elec_cur, max_elec_cur, &init_var)
482    }
483
484    /// Find the steady state where the power (W) is maximized.
485    /// * You need to provide the search range for electrical current (A).
486    /// * You need to provide initial values for optimization by completing [`FullOptVar`](crate::bc::FullOptVar).
487    pub fn maximize_power_in_range_with_manual_init(&self, left_bc: &(dyn BoundaryCondition), right_bc: &(dyn BoundaryCondition), min_elec_cur: f64, max_elec_cur: f64, init_var: &FullOptVar) -> LegSimulationResult {
488        let cost_f = |elec_cur: f64| -> f64 {
489            let result_bc = self.simulate_fixed_elec_cur_with_manual_init(elec_cur, left_bc, right_bc, init_var);
490
491            let cost: f64 = -result_bc.power;
492
493            cost
494        };
495        let optimize_result: OptimizeResult = minimize_brent(&cost_f, min_elec_cur, max_elec_cur, self.simulation_param.max_iter_brent, self.simulation_param.abs_tol_elec_cur_brent);
496        let elec_cur = optimize_result.x[0];
497        let result_bc = self.simulate_fixed_elec_cur_with_manual_init(elec_cur, left_bc, right_bc, init_var);
498
499        result_bc
500    }
501
502    /// Find the steady state where the efficiency (1) is maximized.
503    /// * You need to provide hints for left-end temperature (K) and right-end temperature (K).
504    /// * If this does not converge, use [`SingleLeg::maximize_efficiency_in_range()`] to provide a current range manually.
505    pub fn maximize_efficiency_auto_range(&self, left_bc: &(dyn BoundaryCondition), right_bc: &(dyn BoundaryCondition), templ_hint: f64, tempr_hint: f64) -> LegSimulationResult {
506        let elec_cur_hint: f64 = self.estimate_elec_cur_for_max_efficiency(templ_hint, tempr_hint);
507        let min_elec_cur: f64;
508        let max_elec_cur: f64;
509        if elec_cur_hint > 0.0 {
510            min_elec_cur = elec_cur_hint * 0.5;
511            max_elec_cur = elec_cur_hint * 1.5;
512        } else {
513            min_elec_cur = elec_cur_hint * 1.5;
514            max_elec_cur = elec_cur_hint * 0.5;
515        }
516
517        self.maximize_efficiency_in_range(left_bc, right_bc, min_elec_cur, max_elec_cur, templ_hint, tempr_hint)
518    }
519
520    /// Find the steady state where the efficiency (1) is maximized.
521    /// * You need to provide hints for search range for electrical current (A) and hints for left-end temperature (K) and right-end temperature (K).
522    /// * If this does not converge, use [`SingleLeg::maximize_efficiency_in_range_with_manual_init()`] to provide initial values manually.
523    pub fn maximize_efficiency_in_range(&self, left_bc: &(dyn BoundaryCondition), right_bc: &(dyn BoundaryCondition), min_elec_cur: f64, max_elec_cur: f64, templ_hint: f64, tempr_hint: f64) -> LegSimulationResult {
524        let elec_cur_hint: f64 = (min_elec_cur + max_elec_cur) * 0.5;
525        let elec_cur_density_hint: f64 = elec_cur_hint / self.area;
526        let heat_rate_hint: f64 = self.estimate_heat_flux(templ_hint, tempr_hint, elec_cur_density_hint) * self.area;
527        let init_var: FullOptVar = FullOptVar { templ: templ_hint, heat_ratel: heat_rate_hint, tempr: tempr_hint, heat_rater: heat_rate_hint, elec_cur: elec_cur_hint };
528
529        self.maximize_efficiency_in_range_with_manual_init(left_bc, right_bc, min_elec_cur, max_elec_cur, &init_var)
530    }
531
532    /// Find the steady state where the efficiency (1) is maximized.
533    /// * You need to provide the search range for electrical current (A).
534    /// * You need to provide initial values for optimization by completing [`FullOptVar`](crate::bc::FullOptVar).
535    pub fn maximize_efficiency_in_range_with_manual_init(&self, left_bc: &(dyn BoundaryCondition), right_bc: &(dyn BoundaryCondition), min_elec_cur: f64, max_elec_cur: f64, init_var: &FullOptVar) -> LegSimulationResult {
536        let cost_f = |elec_cur: f64| -> f64 {
537            let result_bc = self.simulate_fixed_elec_cur_with_manual_init(elec_cur, left_bc, right_bc, init_var);
538
539            let cost: f64 = -result_bc.efficiency;
540
541            cost
542        };
543        let optimize_result: OptimizeResult = minimize_brent(&cost_f, min_elec_cur, max_elec_cur, self.simulation_param.max_iter_brent, self.simulation_param.abs_tol_elec_cur_brent);
544        let elec_cur = optimize_result.x[0];
545        let result_bc = self.simulate_fixed_elec_cur_with_manual_init(elec_cur, left_bc, right_bc, init_var);
546
547        result_bc
548    }
549
550    /// Solve the initial-value problem and return the solution matrix (`T`, `q`).
551    /// * The result is a 2x`N` matrix, where `N` is the length of `xs`.
552    fn solve_ivp(&self, elec_cur_density: f64, templ: f64, heat_fluxl: f64, xs: &ColVec) -> Array2<f64> {
553        let f = |_x: f64, y: &ColVec| -> ColVec {
554            teg_rhs(&self.tep, y[0], y[1], elec_cur_density)
555        };
556
557        explicit_rk4(&f, xs, &array![templ, heat_fluxl])
558    }
559
560    /// Solve the final-value problem and return the solution matrix (`T`, `q`).
561    /// * The result is a 2x`N` matrix, where `N` is the length of `xs`.
562    fn solve_fvp(&self, elec_cur_density: f64, tempr: f64, heat_fluxr: f64, xs: &ColVec) -> Array2<f64> {
563        let f = |_x: f64, y: &ColVec| -> ColVec {
564            teg_rhs(&self.tep, y[0], y[1], elec_cur_density)
565        };
566
567        explicit_rk4_fvp(&f, xs, &array![tempr, heat_fluxr])
568    }
569
570    /// Returns the voltage (V) induced from the Seebeck effect
571    fn compute_voltage(&self, templ: f64, tempr: f64) -> f64 {
572        let temp_diff = templ - tempr;
573        if temp_diff == 0.0 {
574            return 0.0;
575        }
576
577        let num_temp_mesh: usize = get_num_mesh(temp_diff.abs(), self.simulation_param.dtemp, self.simulation_param.min_num_temp_mesh, self.simulation_param.max_num_temp_mesh);
578        let temp_mesh: ColVec;
579        let h: f64;
580
581        if tempr < templ {
582            temp_mesh = Array::linspace(tempr, templ, num_temp_mesh);
583            h = temp_mesh[1] - temp_mesh[0];
584        } else {
585            temp_mesh = Array::linspace(templ, tempr, num_temp_mesh);
586            h = temp_mesh[0] - temp_mesh[1];  // Here the mesh size `h` is negative.
587        }
588        
589        integrate_simpson(&self.tep.seebeck_ats_incr(&temp_mesh), h)
590    }
591
592    /// Returns the resistance (Ohm) inside the leg.
593    /// * The argument `temps` must be `T(xs)` where `xs` is a member variable of `SingleLeg` that is a equidistant spatial mesh grid.
594    fn compute_internal_resistance(&self, temps: &ColVec) -> f64 {
595        let rho_of_x: ColVec = self.tep.elec_resi_ats(temps);
596        let h = self.xs[1] - self.xs[0];
597
598        integrate_simpson(&rho_of_x, h) / self.area
599    }
600
601    /// Returns the power (W).
602    #[inline]
603    fn get_power(&self, heat_fluxl: f64, heat_fluxr: f64) -> f64 {
604        (heat_fluxl - heat_fluxr) * self.area
605    }
606
607    /// Returns an estimation of the heat flux
608    pub fn estimate_heat_flux(&self, templ: f64, tempr: f64, elec_cur_density: f64) -> f64 {
609        let alpha = self.tep.seebeck_at(templ);
610        let kappa = self.tep.thrm_cond_at(templ);
611
612        alpha*templ*elec_cur_density - kappa*(tempr-templ)/self.length
613    }
614
615    /// Returns an approximate value of the maximum efficiency.
616    /// * The approximation is performed using the Equation 2--4 in [B. Ryu, J. Chung and S. Park (2021)](https://doi.org/10.1016/j.isci.2021.102934).
617    /// * The approximation is based on the theory of three thermoelectric degrees of freedom suggested by the authors; the theory uses three figures of merit (`Z_gen`, `tau` and `beta`).
618    pub fn estimate_max_efficiency(&self, templ: f64, tempr: f64) -> f64 {
619        let hot_temp: f64;
620        let cold_temp: f64;
621
622        if templ < tempr {
623            cold_temp = templ;
624            hot_temp = tempr;
625        } else {
626            cold_temp = tempr;
627            hot_temp = templ;
628        }
629
630        let delta_temp: f64 = hot_temp - cold_temp;
631        let num_temp_mesh: usize = get_num_mesh(delta_temp, self.simulation_param.dtemp, self.simulation_param.min_num_temp_mesh, self.simulation_param.max_num_temp_mesh);
632        let temp_mesh: ColVec = Array::linspace(cold_temp, hot_temp, num_temp_mesh);
633        let h: f64 = temp_mesh[1] - temp_mesh[0];
634
635        let alpha_bar: f64 = integrate_simpson(&self.tep.seebeck_ats_incr(&temp_mesh), h) / delta_temp;
636        let rho_kappa_bar: f64 = integrate_simpson(&(self.tep.elec_resi_ats_incr(&temp_mesh)*self.tep.thrm_cond_ats_incr(&temp_mesh)), h) / delta_temp;
637
638        // approximate the three degrees of freedom
639        let z_gen: f64 = alpha_bar.powi(2) / rho_kappa_bar;
640        let tau: f64 = (-1.0/6.0) * (self.tep.seebeck_at(hot_temp) - self.tep.seebeck_at(cold_temp)) / alpha_bar;
641        let beta: f64 = (1.0/6.0) * (self.tep.elec_resi_at(hot_temp)*self.tep.thrm_cond_at(hot_temp) - self.tep.elec_resi_at(cold_temp)*self.tep.thrm_cond_at(cold_temp)) / rho_kappa_bar;
642
643        // perturb the temperature range
644        let temp_hp: f64 = hot_temp - tau * delta_temp;
645        let temp_cp: f64 = cold_temp - (tau + beta)*delta_temp;
646        let temp_midp: f64 = (temp_hp+temp_cp)*0.5;
647
648        // approximate the max. efficiency
649        let approx_max_eff = (delta_temp/temp_hp) * ((1.0+z_gen*temp_midp).sqrt()-1.0) / ((1.0+z_gen*temp_midp).sqrt()+(temp_cp/temp_hp));
650
651        approx_max_eff
652    }
653
654    /// Returns an approximate value of the maximum efficiency (1).
655    /// * The approximation is performed using the only `Z_gen` in the Equation 2--4 in [B. Ryu, J. Chung and S. Park (2021)](https://doi.org/10.1016/j.isci.2021.102934); that is, we assume `tau=0` and `beta=0`.
656    /// * This approximation is exact if the Seebeck coefficient is temperature-independent; for its proof, refer to [J. Chung, B. Ryu and H. Seo](https://arxiv.org/abs/2106.10957).
657    pub fn estimate_max_efficiency_by_z(&self, templ: f64, tempr: f64) -> f64 {
658        let hot_temp: f64;
659        let cold_temp: f64;
660
661        if templ < tempr {
662            cold_temp = templ;
663            hot_temp = tempr;
664        } else {
665            cold_temp = tempr;
666            hot_temp = templ;
667        }
668
669        let delta_temp: f64 = hot_temp - cold_temp;
670        let num_temp_mesh: usize = get_num_mesh(delta_temp, self.simulation_param.dtemp, self.simulation_param.min_num_temp_mesh, self.simulation_param.max_num_temp_mesh);
671        let temp_mesh: ColVec = Array::linspace(cold_temp, hot_temp, num_temp_mesh);
672        let h: f64 = temp_mesh[1] - temp_mesh[0];
673
674        let alpha_bar: f64 = integrate_simpson(&self.tep.seebeck_ats_incr(&temp_mesh), h) / delta_temp;
675        let rho_kappa_bar: f64 = integrate_simpson(&(self.tep.elec_resi_ats_incr(&temp_mesh)*self.tep.thrm_cond_ats_incr(&temp_mesh)), h) / delta_temp;
676
677        // approximate the `Z_gen`
678        let z_gen: f64 = alpha_bar.powi(2) / rho_kappa_bar;
679
680        // perturb the temperature range
681        let mid_temp: f64 = (hot_temp + cold_temp) * 0.5;
682
683        // approximate the max. efficiency (`tau=0` and `beta=0` case)
684        let approx_max_eff = (delta_temp/hot_temp) * ((1.0+z_gen*mid_temp).sqrt()-1.0) / ((1.0+z_gen*mid_temp).sqrt()+(cold_temp/hot_temp));
685
686        approx_max_eff
687    }
688
689    /// Returns an approximate value of the maximum power (W).
690    /// * The approximation is performed using the formula `P_max = V^2/(4*R)` from the constant property model, where averaged material properties are used in computation of voltage `V` and internal resistance `R`.
691    /// * Seebeck coefficient is averaged over temperature range.
692    /// * Electrical resistivity is computed by the average of `rho*kappa` divided by the average of `kappa` (`rho` is electrical resistivity and `kappa` is thermal conductivity).
693    pub fn estimate_max_power(&self, templ: f64, tempr: f64) -> f64 {
694        let hot_temp: f64;
695        let cold_temp: f64;
696
697        if templ < tempr {
698            cold_temp = templ;
699            hot_temp = tempr;
700        } else {
701            cold_temp = tempr;
702            hot_temp = templ;
703        }
704
705        let delta_temp: f64 = hot_temp - cold_temp;
706        let num_temp_mesh: usize = get_num_mesh(delta_temp, self.simulation_param.dtemp, self.simulation_param.min_num_temp_mesh, self.simulation_param.max_num_temp_mesh);
707        let temp_mesh: ColVec = Array::linspace(cold_temp, hot_temp, num_temp_mesh);
708        let h: f64 = temp_mesh[1] - temp_mesh[0];
709
710        let alpha_bar: f64 = integrate_simpson(&self.tep.seebeck_ats_incr(&temp_mesh), h) / delta_temp;
711        let rho_kappa_bar: f64 = integrate_simpson(&(self.tep.elec_resi_ats_incr(&temp_mesh)*self.tep.thrm_cond_ats_incr(&temp_mesh)), h) / delta_temp;
712        let kappa_bar: f64 = integrate_simpson(&self.tep.thrm_cond_ats_incr(&temp_mesh), h) / delta_temp;
713
714        // averaged material properties
715        let alpha0: f64 = alpha_bar;
716        let rho0: f64 = rho_kappa_bar / kappa_bar;
717
718        // device parameters
719        let voltage: f64 = alpha0 * delta_temp;
720        let internal_resistance: f64 = rho0 * self.length / self.area;
721
722        // approximate the max. power
723        let approx_max_power = voltage.powi(2) / (4.0 * internal_resistance);
724
725        approx_max_power
726    }
727
728    /// Returns an approximate value of the electrical current (A) attaing the maximum power. If the material is n-type, the return value is negative, representing the reversed direction of the electrical current.
729    /// * The approximation is performed using the formula `I_(P_max) = V/(2*R)`, where averaged material properties are used in computation of voltage `V` and internal resistance `R`.
730    /// * Seebeck coefficient is averaged over temperature range.
731    /// * Electrical resistivity is computed by the average of `rho*kappa` divided by the average of `kappa` (`rho` is electrical resistivity and `kappa` is thermal conductivity).
732    pub fn estimate_elec_cur_for_max_power(&self, templ: f64, tempr: f64) -> f64 {
733        let hot_temp: f64;
734        let cold_temp: f64;
735
736        if templ < tempr {
737            cold_temp = templ;
738            hot_temp = tempr;
739        } else {
740            cold_temp = tempr;
741            hot_temp = templ;
742        }
743
744        let delta_temp: f64 = hot_temp - cold_temp;
745        let num_temp_mesh: usize = get_num_mesh(delta_temp, self.simulation_param.dtemp, self.simulation_param.min_num_temp_mesh, self.simulation_param.max_num_temp_mesh);
746        let temp_mesh: ColVec = Array::linspace(cold_temp, hot_temp, num_temp_mesh);
747        let h: f64 = temp_mesh[1] - temp_mesh[0];
748
749        let alpha_bar: f64 = integrate_simpson(&self.tep.seebeck_ats_incr(&temp_mesh), h) / delta_temp;
750        let rho_kappa_bar: f64 = integrate_simpson(&(self.tep.elec_resi_ats_incr(&temp_mesh)*self.tep.thrm_cond_ats_incr(&temp_mesh)), h) / delta_temp;
751        let kappa_bar: f64 = integrate_simpson(&self.tep.thrm_cond_ats_incr(&temp_mesh), h) / delta_temp;
752
753        // averaged material properties
754        let alpha0: f64 = alpha_bar;
755        let rho0: f64 = rho_kappa_bar / kappa_bar;
756
757        // device parameters
758        let voltage: f64 = alpha0 * delta_temp;
759        let internal_resistance: f64 = rho0 * self.length / self.area;
760
761        // approximate the electrical current
762        let load_ratio = 1.0;
763        let approx_elec_cur_for_max_power = voltage / ((1.0 + load_ratio) * internal_resistance);
764
765        approx_elec_cur_for_max_power
766    }
767
768    /// Returns an approximate value of the electrical current (A) attaing the maximum efficiency. If the material is n-type, the return value is negative, representing the reversed direction of the electrical current.
769    /// * The approximation is performed using the formula `I_(eff_max) = V/((1+sqrt(1+z_gen*Tm))*R)`, where averaged material properties are used in computation of voltage `V` and internal resistance `R`.
770    /// * Seebeck coefficient is averaged over temperature range.
771    /// * Electrical resistivity is computed by the average of `rho*kappa` divided by the average of `kappa` (`rho` is electrical resistivity and `kappa` is thermal conductivity).
772    pub fn estimate_elec_cur_for_max_efficiency(&self, templ: f64, tempr: f64) -> f64 {
773        let hot_temp: f64;
774        let cold_temp: f64;
775
776        if templ < tempr {
777            cold_temp = templ;
778            hot_temp = tempr;
779        } else {
780            cold_temp = tempr;
781            hot_temp = templ;
782        }
783
784        let delta_temp: f64 = hot_temp - cold_temp;
785        let mid_temp: f64 = (hot_temp + cold_temp) * 0.5;
786        let num_temp_mesh: usize = get_num_mesh(delta_temp, self.simulation_param.dtemp, self.simulation_param.min_num_temp_mesh, self.simulation_param.max_num_temp_mesh);
787        let temp_mesh: ColVec = Array::linspace(cold_temp, hot_temp, num_temp_mesh);
788        let h: f64 = temp_mesh[1] - temp_mesh[0];
789
790        let alpha_bar: f64 = integrate_simpson(&self.tep.seebeck_ats_incr(&temp_mesh), h) / delta_temp;
791        let rho_kappa_bar: f64 = integrate_simpson(&(self.tep.elec_resi_ats_incr(&temp_mesh)*self.tep.thrm_cond_ats_incr(&temp_mesh)), h) / delta_temp;
792        let kappa_bar: f64 = integrate_simpson(&self.tep.thrm_cond_ats_incr(&temp_mesh), h) / delta_temp;
793
794        // averaged material properties
795        let alpha0: f64 = alpha_bar;
796        let rho0: f64 = rho_kappa_bar / kappa_bar;
797
798        // device parameters
799        let voltage: f64 = alpha0 * delta_temp;
800        let internal_resistance: f64 = rho0 * self.length / self.area;
801
802        // approximate the `Z_gen`
803        let z_gen: f64 = alpha_bar.powi(2) / rho_kappa_bar;
804
805        // approximate the max. electrical current
806        let load_ratio = (1.0 + z_gen*mid_temp).sqrt();
807        let approx_elec_cur_for_max_efficiency = voltage / ((1.0+load_ratio)*internal_resistance);
808
809        approx_elec_cur_for_max_efficiency
810    }
811}
812
813
814/// Returns the right-hand side of `[T',q']=f(T,q,J)` where `T` is temperature, `q` is heat flux and `J` is electrical current density.
815/// * The derivative is a spatial derivative (position inside a leg).
816/// * For details, refer to p.43 and p.45 in [C. Goupil (ed.)](https://onlinelibrary.wiley.com/doi/book/10.1002/9783527338405).
817/// 
818/// # Arguments
819/// `tep`: Thermoelectric material properties
820/// `temp`: temperature
821/// `heat_flux`: heat flux
822/// `elec_cur_density`: electrical current density flowing through a leg
823fn teg_rhs(tep: &Tep, temp: f64, heat_flux: f64, elec_cur_density: f64) -> ColVec {
824    let alpha = tep.seebeck_at(temp);
825    let rho = tep.elec_resi_at(temp);
826    let kappa = tep.thrm_cond_at(temp);
827    let q = heat_flux;
828    let j = elec_cur_density;
829
830    let dtempdx = (alpha*temp*j - q)/kappa;
831    let dqdx = (alpha*dtempdx + rho*j) * j;
832
833    array![dtempdx, dqdx]
834}
835
836
837/// Returns the number of mesh points satisfying the following conditions:
838/// * If possible, return a close number of meshes having the length of `suggested_mesh_size`.
839/// * The number must be larger than or equal to `min_num_mesh`.
840/// * The number must be larger than or equal to four. This is becuase we use [`PiecewiseCubicPolynomial`](crate::interp::PiecewiseCubicPolynomial).
841/// * The number must be odd. This is because we use [`integrate_simpson`](create::calc::integrate_simpson) for integration.
842/// 
843/// # Panics
844/// * when `total_size` is not positive.
845/// * when `suggested_mesh_size` is not positive.
846fn get_num_mesh(total_size: f64, suggested_mesh_size: f64, min_num_mesh: usize, max_num_mesh: usize) -> usize {
847    if total_size <= 0.0 {
848        panic!("`total_size` must be a positive number!");
849    }
850    if suggested_mesh_size <= 0.0 {
851        panic!("`suggested_mesh_size` must be a positive number!");
852    }
853    let mut num_mesh: usize = (total_size / suggested_mesh_size).ceil() as usize + 1;
854    if num_mesh > max_num_mesh {
855        num_mesh = max_num_mesh - 1;
856    }
857    if num_mesh < min_num_mesh {
858        num_mesh = min_num_mesh;
859    }
860    if num_mesh < 4 {
861        num_mesh = 4;
862    }
863    if num_mesh > max_num_mesh {
864        num_mesh = max_num_mesh - 1;
865    }
866    // if the number is even, make it odd
867    if num_mesh & 1 == 0 {
868        num_mesh += 1;
869    }
870
871    num_mesh
872}
873
874/// Returns the efficiency
875#[inline]
876fn get_efficiency(heat_fluxl: f64, heat_fluxr: f64) -> f64 {
877    1.0 - heat_fluxr/heat_fluxl
878}
879
880
881#[cfg(test)]
882mod tests {
883    use super::*;
884    use std::time::Instant;
885
886    use crate::calc::{all_close, is_close, max};
887    use crate::func::{ConstantFunction, ExplicitFunction};
888    use crate::interp::PiecewiseLinearConstExt;
889    use crate::bc::{FixedTempBc, FixedHeatFluxBc, ConvectionBc};
890
891    /// Test using linearly increasing thermal conductivity.
892    #[test]
893    #[ignore]
894    fn test_estimate_max_power_for_linear_thrm_cond() {
895        // environment condition
896        let hot_temp: f64 = 500.0;
897        let cold_temp: f64 = 300.0;
898
899        // set tep and leg parameters
900        let alpha0: f64 = 150e-6; // [V/K]
901        let rho0: f64 = 1e-5; // [Ohm m]
902        let kappa0: f64 = 2.0; // [W/m/K]
903        let temp0: f64 = 300.0; // [K]
904        let seebeck = ConstantFunction::new(alpha0);
905        let elec_resi = ConstantFunction::new(rho0);
906        let thrm_cond = ExplicitFunction::new(Rc::new(move |temp: f64| { max(kappa0*temp/temp0, kappa0*cold_temp/temp0) }));   // `thrm_cond` must be positive.
907        let tep = Tep::new(Rc::new(seebeck), Rc::new(elec_resi), Rc::new(thrm_cond));
908        let length = 1e-3; // [m]
909        let area = 1e-6;  // m^2]
910
911        // set simulation parameters
912        let minimizer_kind = MinimizerKind::Bfgs;
913        let minimizer_param = MinimizerParam {
914            grad_tol: 1e-5,
915            max_iter: 100,
916            ..MinimizerParam::default(minimizer_kind)
917        };
918        let simul_param = SimulationParam {
919            dx: length/10.0,
920            abs_tol_elec_cur_brent: 1e-10,
921            minimizer_kind,
922            minimizer_param,
923            ..SimulationParam::default()
924        };
925
926        // create a leg
927        let leg = SingleLeg::new(&tep, length, area, simul_param);
928
929        // check the temperature
930        let now = Instant::now();
931        let simul_result = leg.maximize_power_auto_range(&FixedTempBc::new(500.0), &FixedTempBc::new(300.0), hot_temp, cold_temp);
932        println!("test_estimate_max_power_for_linear_thrm_cond: Elapsed {:.2?}", now.elapsed());
933        if !simul_result.success {
934            println!("error: {:?}", simul_result.error);
935        }
936        assert!(simul_result.success);
937        let approx_power: f64 = leg.estimate_max_power(hot_temp, cold_temp);
938        let approx_elec_cur: f64 = leg.estimate_elec_cur_for_max_power(hot_temp, cold_temp);
939        println!("estimated power={}, numerical power={}", approx_power, simul_result.power);
940        assert!(is_close(simul_result.power, approx_power, 5e-06, 0.0));  // 3.0e-6 rel. error, 6.7e-8 abs. error
941        println!("estimated elec_cur={}, numerical elec_cur={}", approx_elec_cur, simul_result.elec_cur);
942        assert!(is_close(approx_elec_cur, simul_result.elec_cur, 5e-06, 0.0));  // 3.6e-6 rel. error, 5.4e-6 abs. error
943    }
944
945    /// Test using linearly increasing thermal conductivity.
946    #[test]
947    #[ignore]
948    fn test_estimate_max_efficiency_for_linear_thrm_cond() {
949        // environment condition
950        let hot_temp: f64 = 500.0;
951        let cold_temp: f64 = 300.0;
952
953        // set tep and leg parameters
954        let alpha0: f64 = 150e-6; // [V/K]
955        let rho0: f64 = 1e-5; // [Ohm m]
956        let kappa0: f64 = 2.0; // [W/m/K]
957        let temp0: f64 = 300.0; // [K]
958        let seebeck = ConstantFunction::new(alpha0);
959        let elec_resi = ConstantFunction::new(rho0);
960        let thrm_cond = ExplicitFunction::new(Rc::new(move |temp: f64| { max(kappa0*temp/temp0, kappa0*cold_temp/temp0) }));   // `thrm_cond` must be positive.
961        let tep = Tep::new(Rc::new(seebeck), Rc::new(elec_resi), Rc::new(thrm_cond));
962        let length = 1e-3; // [m]
963        let area = 1e-6;  // m^2]
964
965        // set simulation parameters
966        let minimizer_kind = MinimizerKind::Bfgs;
967        let minimizer_param = MinimizerParam {
968            grad_tol: 1e-5,
969            max_iter: 100,
970            ..MinimizerParam::default(minimizer_kind)
971        };
972        let simul_param = SimulationParam {
973            dx: length/10.0,
974            abs_tol_elec_cur_brent: 1e-6,
975            minimizer_kind,
976            minimizer_param,
977            ..SimulationParam::default()
978        };
979
980        // create a leg
981        let leg = SingleLeg::new(&tep, length, area, simul_param);
982
983        // check the temperature
984        let now = Instant::now();
985        let simul_result = leg.maximize_efficiency_auto_range(&FixedTempBc::new(500.0), &FixedTempBc::new(300.0), hot_temp, cold_temp);
986        println!("test_estimate_max_efficiency_for_linear_thrm_cond: Elapsed {:.2?}", now.elapsed());
987        if !simul_result.success {
988            println!("error: {:?}", simul_result.error);
989        }
990        assert!(simul_result.success);
991        let approx_eff: f64 = leg.estimate_max_efficiency(hot_temp, cold_temp);
992        let approx_eff_by_z: f64 = leg.estimate_max_efficiency_by_z(hot_temp, cold_temp);
993        let approx_elec_cur: f64 = leg.estimate_elec_cur_for_max_efficiency(hot_temp, cold_temp);
994        println!("estimated eff by Ztb={}, estimated eff by CSM={}, numerical eff={}", approx_eff, approx_eff_by_z, simul_result.efficiency);
995        assert!(is_close(simul_result.efficiency, approx_eff, 2e-03, 0.0));  // 1.3e-3 rel. error, 4.6e-5 abs. error
996        assert!(is_close(simul_result.efficiency, approx_eff_by_z, 5e-06, 0.0));  // 3.6e-6 rel. error, 1.3e-7 abs. error
997        println!("estimated elec_cur={}, numerical elec_cur={}", approx_elec_cur, simul_result.elec_cur);
998        assert!(is_close(approx_elec_cur, simul_result.elec_cur, 5e-05, 0.0));  // 3.0e-5 rel. error, 4.2e-5 abs. error
999    }
1000
1001    /// Test using constant property model.
1002    /// The example is from p.89 in [C. Goupil (ed.)](https://onlinelibrary.wiley.com/doi/book/10.1002/9783527338405).
1003    #[test]
1004    #[ignore]
1005    fn test_maximize_power_for_cpm() {
1006        // environment condition
1007        let hot_temp: f64 = 500.0;
1008        let cold_temp: f64 = 300.0;
1009        let delta_temp = hot_temp - cold_temp;
1010        
1011        // set tep and leg parameters
1012        let alpha0: f64 = 150e-6;
1013        let rho0: f64 = 1e-5;
1014        let kappa0: f64 = 2.0;
1015        let seebeck = ConstantFunction::new(alpha0);
1016        let elec_resi = ConstantFunction::new(rho0);
1017        let thrm_cond = ConstantFunction::new(kappa0);
1018        let tep = Tep::new(Rc::new(seebeck), Rc::new(elec_resi), Rc::new(thrm_cond));
1019        let length = 1e-3;
1020        let area = 1e-6;
1021
1022        // set simulation parameters
1023        let minimizer_kind = MinimizerKind::Bfgs;
1024        let minimizer_param = MinimizerParam {
1025            grad_tol: 1e-5,
1026            a_max: 2.0,
1027            max_iter: 200,
1028            ..MinimizerParam::default(minimizer_kind)
1029        };
1030        let simul_param = SimulationParam {
1031            dx: length/10.0,
1032            dtemp: (hot_temp-cold_temp)/10.0,
1033            abs_tol_elec_cur_brent: 1e-6,
1034            minimizer_kind,
1035            minimizer_param,
1036            ..SimulationParam::default()
1037        };
1038
1039        // create a leg and simulate
1040        let leg = SingleLeg::new(&tep, length, area, simul_param);
1041        let left_bc = FixedTempBc::new(hot_temp);
1042        let right_bc = FixedTempBc::new(cold_temp);
1043
1044        // exact solution
1045        let internal_resistance = rho0*length/area;
1046        let voltage = alpha0*delta_temp;
1047        let max_power = voltage.powi(2)/(4.0 * internal_resistance);
1048        let estimated_elec_cur = voltage/(2.0 * internal_resistance);
1049        println!("estimated_elec_cur={}", estimated_elec_cur);
1050
1051        let now = Instant::now();
1052        // let result_fixed_temp = leg.maximize_power_in_range(&left_bc, &right_bc, estimated_elec_cur*0.8, estimated_elec_cur*1.2, hot_temp, cold_temp);  // 370.3089ms
1053        let result_fixed_temp = leg.maximize_power_auto_range(&left_bc, &right_bc, hot_temp, cold_temp);  // 370.3089ms
1054        println!("test_maximize_power_for_cpm(): elapsed: {:?}", now.elapsed());
1055        assert!(result_fixed_temp.success);
1056
1057        println!("{}", result_fixed_temp);
1058        println!("max_power={}, internal_resistance={}", max_power, internal_resistance);
1059
1060        assert!(is_close(result_fixed_temp.power, max_power, 1e-04, 0.0));  // 0.01% rel. error
1061        assert!(is_close(result_fixed_temp.internal_resistance, internal_resistance, 1e-04, 0.0));
1062    }
1063
1064    /// Test using linearly increasing electrical resistivity.
1065    #[test]
1066    #[ignore]
1067    fn test_maximize_efficiency_for_linear_elec_resi() {
1068        // environment condition
1069        let hot_temp: f64 = 500.0;
1070        let cold_temp: f64 = 300.0;
1071        let delta_temp = hot_temp - cold_temp;
1072        let mid_temp: f64 = (hot_temp+cold_temp) * 0.5;
1073
1074        // set tep and leg parameters
1075        let alpha0: f64 = 150e-6;
1076        let rho0: f64 = 1e-5;
1077        let kappa0: f64 = 2.0;
1078        let temp0: f64 = 400.0;
1079        let seebeck = ConstantFunction::new(alpha0);
1080        let elec_resi = ExplicitFunction::new(Rc::new(move |temp: f64| { max(rho0*temp/temp0, 1e-08) })); // `elec_resi` must be positive.
1081        let thrm_cond = ConstantFunction::new(kappa0);
1082        let tep = Tep::new(Rc::new(seebeck), Rc::new(elec_resi), Rc::new(thrm_cond));
1083        let length = 1e-3;
1084        let area = 1e-6;
1085
1086        // set simulation parameters
1087        let minimizer_kind = MinimizerKind::Bfgs;
1088        let minimizer_param = MinimizerParam {
1089            grad_tol: 1e-7,
1090            a_max: 2.0,
1091            max_iter: 200,
1092            ..MinimizerParam::default(minimizer_kind)
1093        };
1094        let simul_param = SimulationParam {
1095            dx: length/10.0,
1096            dtemp: (hot_temp-cold_temp)/10.0,
1097            abs_tol_elec_cur_brent: 1e-6,
1098            minimizer_kind,
1099            minimizer_param,
1100            ..SimulationParam::default()
1101        };
1102
1103        // create a leg
1104        let leg = SingleLeg::new(&tep, length, area, simul_param);
1105
1106        // simulate the leg
1107        let left_bc = FixedTempBc::new(hot_temp);
1108        let right_bc = FixedTempBc::new(cold_temp);
1109        // let result_fixed_temp = leg.maximize_efficiency_in_range(&left_bc, &right_bc, elec_cur*0.5, elec_cur*1.5, hot_temp, cold_temp);
1110        let result_fixed_temp = leg.maximize_efficiency_auto_range(&left_bc, &right_bc, hot_temp, cold_temp);
1111        assert!(result_fixed_temp.success);
1112        println!("result = {}", result_fixed_temp);
1113        // test helper functions
1114        let load_resistance = result_fixed_temp.load_resistance();
1115        assert!(is_close(result_fixed_temp.total_resistance(), result_fixed_temp.internal_resistance + load_resistance, 1e-05, 0.0));
1116        assert!(is_close(result_fixed_temp.load_ratio(), load_resistance / result_fixed_temp.internal_resistance, 1e-05, 0.0));
1117        // compute exact max. efficiency
1118        let z = alpha0.powi(2)/(rho0*kappa0);
1119        let max_efficiency = (delta_temp/hot_temp) * ((1.0+z*mid_temp).sqrt()-1.0) / ((1.0+z*mid_temp).sqrt()+(cold_temp/hot_temp));
1120        println!("computed eff={}, exact max eff={}", result_fixed_temp.efficiency, max_efficiency);
1121        assert!(is_close(result_fixed_temp.efficiency, max_efficiency, 1e-04, 0.0));  // 0.01% rel. error
1122    }
1123
1124    /// Test using linearly increasing thermal conductivity.
1125    #[test]
1126    #[ignore]
1127    fn test_maximize_efficiency_for_cpm() {
1128        // environment condition
1129        let hot_temp: f64 = 500.0;
1130        let cold_temp: f64 = 300.0;
1131        let delta_temp = hot_temp-cold_temp;
1132        let mid_temp: f64 = (hot_temp+cold_temp) * 0.5;
1133
1134        // set tep and leg parameters
1135        let alpha0: f64 = 150e-6; // [V/K]
1136        let rho0: f64 = 1e-5; // [Ohm m]
1137        let kappa0: f64 = 2.0; // [W/m/K]
1138        let seebeck = ConstantFunction::new(alpha0);
1139        let elec_resi = ConstantFunction::new(rho0);
1140        let thrm_cond = ConstantFunction::new(kappa0);
1141        let tep = Tep::new(Rc::new(seebeck), Rc::new(elec_resi), Rc::new(thrm_cond));
1142        let length = 1e-3; // [m]
1143        let area = 1e-6;  // m^2]
1144
1145        // compute exact max. efficiency
1146        let z = alpha0.powi(2)/(rho0*kappa0);
1147        let max_efficiency = (delta_temp/hot_temp) * ((1.0+z*mid_temp).sqrt()-1.0) / ((1.0+z*mid_temp).sqrt()+(cold_temp/hot_temp));
1148
1149        // set simulation parameters
1150        let minimizer_kind = MinimizerKind::Bfgs;
1151        let minimizer_param = MinimizerParam {
1152            grad_tol: 1e-5,
1153            a_max: 2.0,
1154            max_iter: 200,
1155            ..MinimizerParam::default(minimizer_kind)
1156        };
1157        let simul_param = SimulationParam {
1158            dx: length/10.0,
1159            dtemp: (hot_temp-cold_temp)/10.0,
1160            abs_tol_elec_cur_brent: 1e-6,
1161            minimizer_kind,
1162            minimizer_param,
1163            ..SimulationParam::default()
1164        };
1165
1166        // create a leg
1167        let leg = SingleLeg::new(&tep, length, area, simul_param);
1168
1169        // simulate the leg
1170        let left_bc = FixedTempBc::new(hot_temp);
1171        let right_bc = FixedTempBc::new(cold_temp);
1172        // let result_fixed_temp = leg.maximize_efficiency_in_range(&left_bc, &right_bc, elec_cur*0.8, elec_cur*1.2, hot_temp, cold_temp);
1173        let result_fixed_temp = leg.maximize_efficiency_auto_range(&left_bc, &right_bc, hot_temp, cold_temp);
1174        println!("result={}", result_fixed_temp);
1175        println!("numerical eff={}, exact max eff={}", result_fixed_temp.efficiency, max_efficiency);
1176        assert!(result_fixed_temp.success);
1177        assert!(is_close(result_fixed_temp.efficiency, max_efficiency, 1e-04, 0.0));  // 0.01% rel. error
1178    }
1179
1180    /// Test using constant property model.
1181    #[test]
1182    #[ignore]
1183    fn test_single_leg_simulate_fixed_load_ratio_for_cpm() {
1184        // environment condition
1185        let hot_temp: f64 = 500.0;
1186        let cold_temp: f64 = 300.0;
1187        let delta_temp: f64 = hot_temp - cold_temp;
1188
1189        // set tep and leg parameters
1190        let alpha0: f64 = 150e-6;
1191        let rho0: f64 = 1e-5;
1192        let kappa0: f64 = 2.0;
1193        let seebeck = ConstantFunction::new(alpha0);
1194        let elec_resi = ConstantFunction::new(rho0);
1195        let thrm_cond = ConstantFunction::new(kappa0);
1196        let tep = Tep::new(Rc::new(seebeck), Rc::new(elec_resi), Rc::new(thrm_cond));
1197        let length = 1e-3;
1198        let area = 1e-6;
1199        let elec_cur: f64 = 1.0;
1200
1201        // set simulation parameters
1202        let minimizer_kind = MinimizerKind::DampedBfgs;
1203        let minimizer_param = MinimizerParam {
1204            grad_tol: 1e-7,
1205            a_max: 2.0,
1206            max_iter: 200,
1207            ..MinimizerParam::default(minimizer_kind)
1208        };
1209        let simul_param = SimulationParam {
1210            dx: length/10.0,
1211            dtemp: delta_temp/10.0,
1212            minimizer_kind,
1213            minimizer_param,
1214            ..SimulationParam::default()
1215        };
1216
1217        // create a leg
1218        let leg = SingleLeg::new(&tep, length, area, simul_param);
1219
1220        // case 1: fixed temperatures
1221        let left_bc = FixedTempBc::new(hot_temp);
1222        let right_bc = FixedTempBc::new(cold_temp);
1223        let simul_result_fixed_elec_cur = leg.simulate_fixed_elec_cur(elec_cur, &left_bc, &right_bc, hot_temp, cold_temp);
1224        assert!(simul_result_fixed_elec_cur.success);
1225        let fixed_load_ratio = simul_result_fixed_elec_cur.load_ratio();
1226
1227        // test a time-consuming method
1228        let now = Instant::now();
1229        // let simul_result_fixed_load_ratio = leg.simulate_fixed_load_ratio(simul_result_fixed_elec_cur.load_ratio(), &left_bc, &right_bc, elec_cur, hot_temp, cold_temp);
1230        let simul_result_fixed_load_ratio = leg.simulate_fixed_load_ratio(fixed_load_ratio, &left_bc, &right_bc, elec_cur/2.0, hot_temp, cold_temp);
1231        println!("For fixed BC and fixed load ratio: Elapsed {:.2?}", now.elapsed());
1232        assert!(simul_result_fixed_load_ratio.success);
1233        assert!(is_close(simul_result_fixed_load_ratio.load_ratio(), fixed_load_ratio, 2e-04, 0.0));
1234        assert!(is_close(simul_result_fixed_load_ratio.elec_cur, elec_cur, 2e-04, 0.0));
1235        println!("result={}", simul_result_fixed_load_ratio);
1236        println!("computed load_ratio={}", simul_result_fixed_load_ratio.load_ratio());
1237
1238        // test rtg
1239        let left_bc = FixedHeatFluxBc::new(100.0);
1240        let right_bc = ConvectionBc::new(30.0, 8.0);
1241        // let fixed_load_ratio = 0.2;
1242        let now = Instant::now();
1243        let simul_result_fixed_load_ratio = leg.simulate_fixed_load_ratio(fixed_load_ratio, &left_bc, &right_bc, elec_cur, hot_temp, cold_temp);
1244        println!("For RTG BC and fixed load ratio: Elapsed {:.2?}", now.elapsed());
1245        println!("result={}", simul_result_fixed_load_ratio);
1246        println!("computed load_ratio={}", simul_result_fixed_load_ratio.load_ratio());
1247        println!("templ={}, tempr={}", simul_result_fixed_load_ratio.temp.at(0.0), simul_result_fixed_load_ratio.temp.at(leg.length));
1248        if !simul_result_fixed_load_ratio.success {
1249            println!("rtg error={:?}", simul_result_fixed_load_ratio.error);
1250        }
1251        //assert!(simul_result_fixed_load_ratio.success);
1252        assert!(is_close(simul_result_fixed_load_ratio.load_ratio(), fixed_load_ratio, 2e-04, 0.0));
1253    }
1254
1255    /// Test using temperature-dependent electrical resistivity.
1256    /// The example is from p.123 in [C. Goupil (ed.)](https://onlinelibrary.wiley.com/doi/book/10.1002/9783527338405).
1257    #[test]
1258    #[ignore]
1259    fn test_single_leg_simulate_fixed_load_resistance_for_linear_elec_resi() {
1260        // environment condition
1261        let hot_temp: f64 = 500.0;
1262        let cold_temp: f64 = 300.0;
1263
1264        // set tep and leg parameters
1265        let alpha0: f64 = 150e-6;
1266        let rho0: f64 = 1e-5;
1267        let temp0: f64 = 400.0;
1268        let kappa0: f64 = 2.0;
1269        let seebeck = PiecewiseLinearConstExt::new(&array![0.0, cold_temp, hot_temp], &array![0.0, alpha0, alpha0], 0.0, alpha0);  // cut off below `cold_temp`
1270        let elec_resi = ExplicitFunction::new(Rc::new(move |temp: f64| {
1271            if temp > hot_temp {
1272                return rho0*hot_temp/temp0;  // avoid infinitely growing value
1273            }
1274            max(rho0*temp/temp0, rho0*cold_temp/temp0)   // `elec_resi` must be positive.
1275        }));
1276        let thrm_cond = ConstantFunction::new(kappa0);
1277        let tep = Tep::new(Rc::new(seebeck), Rc::new(elec_resi), Rc::new(thrm_cond));
1278        let length = 1e-3;
1279        let area = 1e-6;
1280        let elec_cur: f64 = 1.0;
1281        let j: f64 = elec_cur / area;  // electrical current density (W/m^2)
1282
1283        // set simulation parameters
1284        let minimizer_kind = MinimizerKind::Bfgs;
1285        let minimizer_param = MinimizerParam {
1286            grad_tol: 1e-5,
1287            a_max: 2.0,
1288            max_iter: 200,
1289            ..MinimizerParam::default(minimizer_kind)
1290        };
1291        let simul_param = SimulationParam {
1292            dx: length/10.0,
1293            dtemp: (hot_temp-cold_temp)/10.0,
1294            minimizer_kind,
1295            minimizer_param,
1296            ..SimulationParam::default()
1297        };
1298
1299        // fix an exact solution
1300        let num_x_mesh: usize = 31;
1301        let x_mesh: ColVec = Array::linspace(0.0, length, num_x_mesh);
1302        let omega: f64 = (rho0/(kappa0*temp0)).sqrt() * j;
1303        let cos_omega_x: ColVec = x_mesh.mapv(|x| (omega*x).cos());
1304        let sin_omega_x: ColVec = x_mesh.mapv(|x| (omega*x).sin());
1305        let temp_exact: ColVec = hot_temp * &cos_omega_x + (cold_temp - hot_temp*(omega*length).cos())/(omega*length).sin() * &sin_omega_x;
1306        let dtempdx_exact: ColVec = -hot_temp * omega * &sin_omega_x + (cold_temp - hot_temp*(omega*length).cos())/(omega*length).sin() * omega * &cos_omega_x;
1307        let heat_flux_exact: ColVec = alpha0*&temp_exact*j - kappa0*&dtempdx_exact;
1308
1309        // create a leg
1310        let leg = SingleLeg::new(&tep, length, area, simul_param);
1311
1312        // case 1: fixed temperatures
1313        let left_bc = FixedTempBc::new(hot_temp);
1314        let right_bc = FixedTempBc::new(cold_temp);
1315        let simul_result_fixed_elec_cur = leg.simulate_fixed_elec_cur(elec_cur, &left_bc, &right_bc, hot_temp, cold_temp);
1316        assert!(simul_result_fixed_elec_cur.success);
1317        // test helper functions
1318        let load_resistance = simul_result_fixed_elec_cur.load_resistance();
1319        assert!(is_close(simul_result_fixed_elec_cur.total_resistance(), simul_result_fixed_elec_cur.internal_resistance + load_resistance, 1e-04, 0.0));
1320        assert!(is_close(simul_result_fixed_elec_cur.load_ratio(), load_resistance / simul_result_fixed_elec_cur.internal_resistance, 1e-04, 1.0));
1321        // check if `simulate_fixed_load_resistance` works
1322        // let simul_result_fixed_load_resistance = leg.simulate_fixed_load_resistance_with_manual_init(load_resistance, &left_bc, &right_bc, 0.0, 2.0, hot_temp, 1000.0);
1323        let simul_result_fixed_load_resistance = leg.simulate_fixed_load_resistance(load_resistance, &left_bc, &right_bc, 0.5, hot_temp, cold_temp);
1324        println!("dirichlet; numerical elec_cur={}, exact elec_cur={}", simul_result_fixed_load_resistance.elec_cur, elec_cur);
1325        println!("dirichlet; numerical load resistance={}, exact load resistance={}", simul_result_fixed_load_resistance.load_resistance(), load_resistance);
1326        println!("dirichlet; result={}", simul_result_fixed_load_resistance);
1327        assert!(simul_result_fixed_load_resistance.success);
1328        assert!(is_close(simul_result_fixed_load_resistance.elec_cur, elec_cur, 2e-03, 0.0));  // 0.2% rel. error
1329        assert!(is_close(simul_result_fixed_load_resistance.load_resistance(), load_resistance, 2e-03, 0.0));
1330
1331        // case 2: RTG BC
1332        let heat_fluxl: f64 = heat_flux_exact[0];
1333        let fluid_temp: f64 = cold_temp - 100.0;
1334        let h: f64 = heat_flux_exact[num_x_mesh-1] / (temp_exact[num_x_mesh-1] - fluid_temp);
1335        let left_bc = FixedHeatFluxBc::new(heat_fluxl);
1336        let right_bc = ConvectionBc::new(h, fluid_temp);
1337        let elec_cur: f64 = 1.2;
1338        let simul_result_fixed_elec_cur = leg.simulate_fixed_elec_cur(elec_cur, &left_bc, &right_bc, hot_temp, cold_temp);
1339        assert!(simul_result_fixed_elec_cur.success);
1340        // println!("rtg templ={}, tempr={}, heat_ratel={}, heat_rater={}", simul_result_fixed_elec_cur.templ, simul_result_fixed_elec_cur.tempr, simul_result_fixed_elec_cur.heat_fluxl*leg.area, simul_result_fixed_elec_cur.heat_fluxr*leg.area);
1341        println!("rtg result={}", simul_result_fixed_elec_cur);
1342        let load_resistance = simul_result_fixed_elec_cur.load_resistance();
1343        // let simul_result_fixed_load_resistance = leg.simulate_fixed_load_resistance_with_manual_init(load_resistance, &left_bc, &right_bc, 0.0, 2.0, 700.0, 3600.0);
1344        // let simul_result_fixed_load_resistance = leg.simulate_fixed_load_resistance(load_resistance, &left_bc, &right_bc, 1.0, hot_temp, cold_temp);
1345        let simul_result_fixed_load_resistance = leg.simulate_fixed_load_resistance(load_resistance, &left_bc, &right_bc, elec_cur*0.9, hot_temp, cold_temp);
1346        // let simul_result_fixed_load_resistance = leg.obsolete_simulate_fixed_load_resistance(load_resistance, &left_bc, &right_bc, elec_cur*2.0, hot_temp, cold_temp);
1347        println!("rtg; numerical elec_cur={}, exact elec_cur={}", simul_result_fixed_load_resistance.elec_cur, elec_cur);
1348        println!("rtg; numerical load resistance={}, exact load resistance={}", simul_result_fixed_load_resistance.load_resistance(), load_resistance);
1349        println!("rtg; result={}", simul_result_fixed_load_resistance);
1350        assert!(simul_result_fixed_load_resistance.success);
1351        assert!(is_close(simul_result_fixed_load_resistance.elec_cur, elec_cur, 2e-03, 0.0));  // 0.2% rel. error
1352        assert!(is_close(simul_result_fixed_load_resistance.load_resistance(), load_resistance, 2e-03, 0.0));
1353    }
1354
1355    /// Test using logarithmic Seebeck coefficient.
1356    /// The example is from p.112 in [C. Goupil (ed.)](https://onlinelibrary.wiley.com/doi/book/10.1002/9783527338405).
1357    #[test]
1358    #[ignore]
1359    fn test_single_leg_simulate_fixed_elec_cur_for_log_seebeck() {
1360        // environment condition
1361        let hot_temp: f64 = 500.0;
1362        let cold_temp: f64 = 300.0;
1363
1364        // set tep and leg parameters
1365        let tau0: f64 = 150e-6;
1366        let rho0: f64 = 1e-5;
1367        let kappa0: f64 = 2.0;
1368        let seebeck = ExplicitFunction::new(Rc::new(move |temp: f64| { tau0*(max(temp/cold_temp, 1.0)).ln() }));   // constant Thomson
1369        let elec_resi = ConstantFunction::new(rho0);
1370        let thrm_cond = ConstantFunction::new(kappa0);
1371        let tep = Tep::new(Rc::new(seebeck), Rc::new(elec_resi), Rc::new(thrm_cond));
1372        let length = 1e-3;
1373        let area = 1e-6;
1374        let elec_cur: f64 = 1.0;
1375        let j: f64 = elec_cur / area;  // electrical current density (W/m^2)
1376
1377        // set simulation parameters
1378        let minimizer_kind = MinimizerKind::Bfgs;
1379        let minimizer_param = MinimizerParam {
1380            grad_tol: 1e-5,
1381            max_iter: 100,
1382            ..MinimizerParam::default(minimizer_kind)
1383        };
1384        let simul_param = SimulationParam {
1385            dx: length/10.0,
1386            minimizer_kind,
1387            minimizer_param,
1388            ..SimulationParam::default()
1389        };
1390
1391        // fix an exact solution
1392        let num_x_mesh: usize = 31;
1393        let x_mesh: ColVec = Array::linspace(0.0, length, num_x_mesh);
1394        let temp_exact: ColVec = hot_temp + ((cold_temp-hot_temp) - (rho0*j/tau0)*length)/(1.0-(tau0*j*length/kappa0).exp()) * (1.0 - x_mesh.mapv(|x| { (tau0*j*x/kappa0).exp() })) + rho0*j/tau0*&x_mesh;
1395        assert!(all_close(&array![temp_exact[0], temp_exact[num_x_mesh-1]], &array![hot_temp, cold_temp], 1e-04, 0.0));
1396
1397        // create a leg
1398        let leg = SingleLeg::new(&tep, length, area, simul_param);
1399
1400        // check the temperature
1401        let simul_result = leg.simulate_fixed_elec_cur(elec_cur, &FixedTempBc::new(hot_temp), &FixedTempBc::new(cold_temp), hot_temp, cold_temp);
1402        assert!(simul_result.success);
1403        assert!(all_close(&simul_result.temp.ats_incr(&x_mesh), &temp_exact, 1e-04, 0.0));
1404    }
1405
1406    /// Test using linearly increasing electrical resistivity.
1407    /// The example is from p.123 in [C. Goupil (ed.)](https://onlinelibrary.wiley.com/doi/book/10.1002/9783527338405).
1408    #[test]
1409    #[ignore]
1410    fn test_single_leg_simulate_fixed_elec_cur_for_linear_elec_resi() {
1411        // environment condition
1412        let hot_temp: f64 = 500.0;
1413        let cold_temp: f64 = 300.0;
1414
1415        // set tep and leg parameters
1416        let alpha0: f64 = 150e-6;
1417        let rho0: f64 = 1e-5;
1418        let kappa0: f64 = 2.0;
1419        let temp0: f64 = 400.0;
1420        let seebeck = PiecewiseLinearConstExt::new(&array![0.0, cold_temp, hot_temp], &array![0.0, alpha0, alpha0], 0.0, alpha0); // cut off below `cold_temp`
1421        let elec_resi = ExplicitFunction::new(Rc::new(move |temp: f64| { max(rho0*temp/temp0, rho0*cold_temp/temp0) }));
1422        let thrm_cond = ConstantFunction::new(kappa0);
1423        let tep = Tep::new(Rc::new(seebeck), Rc::new(elec_resi), Rc::new(thrm_cond));
1424        let length = 1e-3;
1425        let area = 1e-6;
1426        let elec_cur: f64 = 1.0;
1427        let j: f64 = elec_cur / area;  // electrical current density (W/m^2)
1428
1429        // set simulation parameters
1430        // let minimizer_kind = MinimizerKind::DampedBfgs;
1431        let minimizer_kind = MinimizerKind::Bfgs;
1432        let minimizer_param = MinimizerParam {
1433            grad_tol: 1e-5,
1434            max_iter: 100,
1435            ..MinimizerParam::default(minimizer_kind)
1436        };
1437        let simul_param = SimulationParam {
1438            dx: length/10.0,
1439            minimizer_kind,
1440            minimizer_param,
1441            ..SimulationParam::default()
1442        };
1443
1444        // fix an exact solution
1445        let num_x_mesh: usize = 31;
1446        let x_mesh: ColVec = Array::linspace(0.0, length, num_x_mesh);
1447        let omega: f64 = (rho0/(kappa0*temp0)).sqrt() * j;
1448        let cos_omega_x: ColVec = x_mesh.mapv(|x| (omega*x).cos());
1449        let sin_omega_x: ColVec = x_mesh.mapv(|x| (omega*x).sin());
1450        let temp_exact: ColVec = hot_temp * &cos_omega_x + (cold_temp - hot_temp*(omega*length).cos())/(omega*length).sin() * &sin_omega_x;
1451        let dtempdx_exact: ColVec = -hot_temp * omega * &sin_omega_x + (cold_temp - hot_temp*(omega*length).cos())/(omega*length).sin() * omega * &cos_omega_x;
1452        let heat_flux_exact: ColVec = alpha0*&temp_exact*j - kappa0*&dtempdx_exact;
1453
1454        // create a leg
1455        let leg = SingleLeg::new(&tep, length, area, simul_param);
1456
1457        // case 1: fixed temperatures
1458        let now = Instant::now();
1459        let dirichlet_simul_result = leg.simulate_fixed_elec_cur(elec_cur, &FixedTempBc::new(hot_temp), &FixedTempBc::new(cold_temp), hot_temp, cold_temp);
1460        println!("test_single_leg_simulate_fixed_elec_cur_for_linear_elec_resi--case 1: Elapsed {:.2?}", now.elapsed());
1461        if !dirichlet_simul_result.success {
1462            println!("dirichlet failed: error={:?}", dirichlet_simul_result.error);
1463        }
1464        assert!(dirichlet_simul_result.success);
1465        assert!(all_close(&dirichlet_simul_result.temp.ats_incr(&x_mesh), &temp_exact, 1e-03, 0.0));
1466        assert!(all_close(&dirichlet_simul_result.heat_flux.ats_incr(&x_mesh), &heat_flux_exact, 1e-03, 0.0));
1467
1468        // case 2: RTG BC
1469        let heat_fluxl: f64 = heat_flux_exact[0];
1470        let fluid_temp: f64 = cold_temp - 100.0;
1471        let h: f64 = heat_flux_exact[num_x_mesh-1] / (temp_exact[num_x_mesh-1] - fluid_temp);
1472        let now = Instant::now();
1473        let rtg_simul_result = leg.simulate_fixed_elec_cur(elec_cur, &FixedHeatFluxBc::new(heat_fluxl), &ConvectionBc::new(h, fluid_temp), hot_temp, cold_temp);
1474        println!("test_single_leg_simulate_fixed_elec_cur_for_linear_elec_resi--case 2: Elapsed {:.2?}", now.elapsed());
1475        if !rtg_simul_result.success {
1476            println!("rtg failed: error={:?}", rtg_simul_result.error);
1477        }
1478        assert!(rtg_simul_result.success);
1479        println!("rtg numerical temp = {}", rtg_simul_result.temp.ats_incr(&x_mesh));
1480        println!("rtg exact temp = {}", temp_exact);
1481        println!("rtg numerical heat_flux = {}", rtg_simul_result.heat_flux.ats_incr(&x_mesh));
1482        println!("rtg exact heat_flux = {}", heat_flux_exact);
1483        assert!(all_close(&rtg_simul_result.temp.ats_incr(&x_mesh), &temp_exact, 1e-03, 0.0));
1484        assert!(all_close(&rtg_simul_result.heat_flux.ats_incr(&x_mesh), &heat_flux_exact, 1e-03, 0.0));
1485    }
1486
1487    /// Test using linearly increasing thermal conductivity.
1488    /// The example is from p.125 in [C. Goupil (ed.)](https://onlinelibrary.wiley.com/doi/book/10.1002/9783527338405).
1489    #[test]
1490    #[ignore]
1491    fn test_single_leg_simulate_fixed_elec_cur_for_linear_thrm_cond() {
1492        // environment condition
1493        let hot_temp: f64 = 500.0;
1494        let cold_temp: f64 = 300.0;
1495
1496        // set tep and leg parameters
1497        let alpha0: f64 = 150e-6; // [V/K]
1498        let rho0: f64 = 1e-5; // [Ohm m]
1499        let kappa0: f64 = 2.0; // [W/m/K]
1500        let temp0: f64 = 300.0; // [K]
1501        let seebeck = ConstantFunction::new(alpha0);
1502        let elec_resi = ConstantFunction::new(rho0);
1503        let thrm_cond = ExplicitFunction::new(Rc::new(move |temp: f64| { max(kappa0*temp/temp0, kappa0*cold_temp/temp0) }));   // `thrm_cond` must be positive.
1504        let tep = Tep::new(Rc::new(seebeck), Rc::new(elec_resi), Rc::new(thrm_cond));
1505        let length = 1e-3; // [m]
1506        let area = 1e-6;  // m^2]
1507        let elec_cur: f64 = 1.0;
1508        let j: f64 = elec_cur / area;  // electrical current density (W/m^2)
1509
1510        // set simulation parameters
1511        let minimizer_kind = MinimizerKind::Bfgs;
1512        let minimizer_param = MinimizerParam {
1513            grad_tol: 1e-5,
1514            max_iter: 100,
1515            ..MinimizerParam::default(minimizer_kind)
1516        };
1517        let simul_param = SimulationParam {
1518            dx: length/10.0,
1519            minimizer_kind,
1520            minimizer_param,
1521            ..SimulationParam::default()
1522        };
1523
1524        // fix an exact solution
1525        let num_x_mesh: usize = 31;
1526        let x_mesh: ColVec = Array::linspace(0.0, length, num_x_mesh);
1527        let temp_exact: ColVec = ( hot_temp.powi(2) + (cold_temp.powi(2)-hot_temp.powi(2))*&x_mesh/length + j.powi(2)*temp0*rho0/kappa0*&x_mesh*(length-&x_mesh) ).mapv(|e| {e.sqrt()});
1528        assert!(all_close(&array![temp_exact[0], temp_exact[num_x_mesh-1]], &array![hot_temp, cold_temp], 0.0, 0.0)); // the BC is exact
1529
1530        // create a leg
1531        let leg = SingleLeg::new(&tep, length, area, simul_param);
1532
1533        // check the temperature
1534        let now = Instant::now();
1535        let simul_result = leg.simulate_fixed_elec_cur(elec_cur, &FixedTempBc::new(hot_temp), &FixedTempBc::new(cold_temp), hot_temp, cold_temp);
1536        println!("test_single_leg_simulate_fixed_elec_cur_for_linear_thrm_cond: Elapsed {:.2?}", now.elapsed());
1537        if !simul_result.success {
1538            println!("error: {:?}", simul_result.error);
1539        }
1540        assert!(simul_result.success);
1541        println!("result = {}", simul_result);
1542        println!("num_iter = {}", simul_result.num_iter);
1543        println!("fixed cur, linear_thrm_cond={}", simul_result);
1544        println!("temps={}", simul_result.temp.ats_incr(&x_mesh));
1545        assert!(all_close(&simul_result.temp.ats_incr(&x_mesh), &temp_exact, 2e-04, 0.0));  // 16 iterations
1546    }
1547
1548    #[test]
1549    fn test_get_num_mesh() {
1550        assert_eq!(get_num_mesh(10.0, 1.0, 5, 11), 11);
1551        assert_eq!(get_num_mesh(10.0, 10.0, 3, 11), 5);
1552    }
1553}