@@ -10,7 +10,11 @@ use ratex_types::display_item::DisplayList;
1010
1111use super :: i18n:: tr;
1212use super :: { FormulaReferenceSection , ModelChoice , ModelFormulaInfo , ResolvedModel , UiLanguage } ;
13- use crate :: domain:: { MAX_RATIONAL_DEGREE , MIN_RATIONAL_DEGREE } ;
13+ use crate :: domain:: DEFAULT_SATURATING_TREND_TAUS_YEARS ;
14+ use crate :: domain:: {
15+ MAX_RATIONAL_DEGREE , MAX_SATURATING_TREND_TAU_COUNT , MIN_RATIONAL_DEGREE ,
16+ MIN_SATURATING_TREND_TAU_COUNT ,
17+ } ;
1418use crate :: fit:: OptimizationLossMetric ;
1519
1620// Держим размер формулы на уровне основного текста интерфейса.
@@ -75,6 +79,8 @@ fn model_formula_source(
7579 model : ModelChoice ,
7680 polynomial_degree : usize ,
7781 rational_degree : usize ,
82+ saturating_trend_tau_count : usize ,
83+ saturating_trend_taus : Option < & [ f64 ] > ,
7884) -> FormulaSource {
7985 match model {
8086 ModelChoice :: Polynomial => {
@@ -147,6 +153,10 @@ L(x; x_0, \gamma) &= \frac{1}{1 + (\frac{x - x_0}{\gamma})^{2}}
147153 G(x; x_0, σ) = exp(-((x - x_0)^2)∕(2·σ^2))\n \
148154 L(x; x_0, γ) = 1∕(1 + ((x - x_0)∕γ)^2)",
149155 ) ,
156+ ModelChoice :: SaturatingTrendBasis => FormulaSource :: explicit (
157+ & saturating_trend_formula_latex ( saturating_trend_tau_count, saturating_trend_taus) ,
158+ & saturating_trend_formula_plain_text ( saturating_trend_tau_count, saturating_trend_taus) ,
159+ ) ,
150160 ModelChoice :: LinearSpline => FormulaSource :: single (
151161 r"y(x) = y_{i} + \frac{y_{i+1} - y_{i}}{x_{i+1} - x_{i}} \cdot (x - x_{i})" ,
152162 ) ,
@@ -170,9 +180,17 @@ pub(super) fn model_formula_info(
170180 model : ModelChoice ,
171181 polynomial_degree : usize ,
172182 rational_degree : usize ,
183+ saturating_trend_tau_count : usize ,
184+ saturating_trend_taus : Option < & [ f64 ] > ,
173185 optimization_metric : OptimizationLossMetric ,
174186) -> ModelFormulaInfo {
175- let model_formula = model_formula_source ( model, polynomial_degree, rational_degree) ;
187+ let model_formula = model_formula_source (
188+ model,
189+ polynomial_degree,
190+ rational_degree,
191+ saturating_trend_tau_count,
192+ saturating_trend_taus,
193+ ) ;
176194 let model_section = FormulaReferenceSection {
177195 title : tr ( language, "Model equation" , "Уравнение модели" ) . to_string ( ) ,
178196 render_latex : model_formula. render_latex . clone ( ) ,
@@ -182,6 +200,7 @@ pub(super) fn model_formula_info(
182200 model,
183201 polynomial_degree,
184202 rational_degree,
203+ saturating_trend_tau_count,
185204 ) ,
186205 } ;
187206
@@ -253,8 +272,14 @@ fn model_reference_description(
253272 model : ModelChoice ,
254273 polynomial_degree : usize ,
255274 rational_degree : usize ,
275+ saturating_trend_tau_count : usize ,
256276) -> String {
257- let min_points = model_min_points ( model, polynomial_degree, rational_degree) ;
277+ let min_points = model_min_points (
278+ model,
279+ polynomial_degree,
280+ rational_degree,
281+ saturating_trend_tau_count,
282+ ) ;
258283 let mut description = format ! (
259284 "{}: {min_points}\n {}: x - {}, y - {}" ,
260285 tr( language, "Minimum points" , "Минимум точек" ) ,
@@ -366,15 +391,69 @@ fn reference_plain_text(sections: &[FormulaReferenceSection]) -> String {
366391 output
367392}
368393
369- fn model_min_points ( model : ModelChoice , polynomial_degree : usize , rational_degree : usize ) -> usize {
370- match ResolvedModel :: from_choice ( model, polynomial_degree, rational_degree) {
394+ fn model_min_points (
395+ model : ModelChoice ,
396+ polynomial_degree : usize ,
397+ rational_degree : usize ,
398+ saturating_trend_tau_count : usize ,
399+ ) -> usize {
400+ match ResolvedModel :: from_choice (
401+ model,
402+ polynomial_degree,
403+ rational_degree,
404+ saturating_trend_tau_count,
405+ ) {
371406 ResolvedModel :: Parametric ( family) => family. min_points ( ) ,
372407 ResolvedModel :: LinearSpline | ResolvedModel :: MonotoneCubicSpline => 2 ,
373408 ResolvedModel :: NaturalCubicSpline => 3 ,
374409 ResolvedModel :: AkimaSpline => 5 ,
375410 }
376411}
377412
413+ fn saturating_trend_tau_values ( tau_count : usize ) -> & ' static [ f64 ] {
414+ let clamped = tau_count. clamp (
415+ MIN_SATURATING_TREND_TAU_COUNT ,
416+ MAX_SATURATING_TREND_TAU_COUNT ,
417+ ) ;
418+ & DEFAULT_SATURATING_TREND_TAUS_YEARS [ ..clamped]
419+ }
420+
421+ fn saturating_trend_formula_latex ( tau_count : usize , custom_taus : Option < & [ f64 ] > ) -> String {
422+ let taus = custom_taus. unwrap_or_else ( || saturating_trend_tau_values ( tau_count) ) ;
423+ let taus = taus
424+ . iter ( )
425+ . map ( |tau| trim_float_for_formula ( * tau) )
426+ . collect :: < Vec < _ > > ( )
427+ . join ( ", " ) ;
428+ let count = custom_taus
429+ . unwrap_or_else ( || saturating_trend_tau_values ( tau_count) )
430+ . len ( ) ;
431+ format ! (
432+ r"y = c + \sum_{{i=1}}^{{{count}}} w_{{i}} \cdot (1 - \exp(-\frac{{x}}{{\tau_i}})), \quad \tau_i \in \{{{taus}\}}"
433+ )
434+ }
435+
436+ fn saturating_trend_formula_plain_text ( tau_count : usize , custom_taus : Option < & [ f64 ] > ) -> String {
437+ let taus = custom_taus. unwrap_or_else ( || saturating_trend_tau_values ( tau_count) ) ;
438+ let taus = taus
439+ . iter ( )
440+ . map ( |tau| trim_float_for_formula ( * tau) )
441+ . collect :: < Vec < _ > > ( )
442+ . join ( ", " ) ;
443+ let count = custom_taus
444+ . unwrap_or_else ( || saturating_trend_tau_values ( tau_count) )
445+ . len ( ) ;
446+ format ! ( "y = c + sum_{{i=1..{count}}}(w_i·(1 - exp(-x/τ_i))), τ_i in {{{taus}}}" )
447+ }
448+
449+ fn trim_float_for_formula ( value : f64 ) -> String {
450+ let formatted = format ! ( "{value:.2}" ) ;
451+ formatted
452+ . trim_end_matches ( '0' )
453+ . trim_end_matches ( '.' )
454+ . to_string ( )
455+ }
456+
378457fn model_constraint_note ( language : UiLanguage , model : ModelChoice ) -> Option < & ' static str > {
379458 match model {
380459 ModelChoice :: Arrhenius
@@ -429,6 +508,9 @@ fn model_ml_note(language: UiLanguage, model: ModelChoice) -> &'static str {
429508 ( UiLanguage :: English , ModelChoice :: PseudoVoigt ) => {
430509 "Mixture of Gaussian and Lorentzian peaks with learnable blend."
431510 }
511+ ( UiLanguage :: English , ModelChoice :: SaturatingTrendBasis ) => {
512+ "Fixed saturating basis over a preset tau grid; x is interpreted in years."
513+ }
432514 ( UiLanguage :: English , ModelChoice :: HyperbolicTangent ) => {
433515 "Smooth S-curve transition with bounded tails."
434516 }
@@ -490,6 +572,9 @@ fn model_ml_note(language: UiLanguage, model: ModelChoice) -> &'static str {
490572 ( UiLanguage :: Russian , ModelChoice :: PseudoVoigt ) => {
491573 "Смесь гауссового и лоренцевого пиков с обучаемой долей."
492574 }
575+ ( UiLanguage :: Russian , ModelChoice :: SaturatingTrendBasis ) => {
576+ "Фиксированный базис насыщения по сетке tau; x интерпретируется как годы."
577+ }
493578 ( UiLanguage :: Russian , ModelChoice :: HyperbolicTangent ) => {
494579 "Гладкий S-переход с ограниченными хвостами."
495580 }
@@ -856,18 +941,34 @@ mod tests {
856941 #[ test]
857942 fn every_model_formula_renders_as_svg ( ) {
858943 for model in ModelChoice :: ALL {
859- let degree_pairs = if model. is_polynomial ( ) {
860- [ ( 1_usize , 1_usize ) , ( 9 , 1 ) ] . as_slice ( )
944+ let degree_triplets = if model. is_polynomial ( ) {
945+ [
946+ ( 1_usize , 1_usize , MAX_SATURATING_TREND_TAU_COUNT ) ,
947+ ( 9 , 1 , MAX_SATURATING_TREND_TAU_COUNT ) ,
948+ ]
949+ . as_slice ( )
861950 } else if model. is_rational ( ) {
862- [ ( 1_usize , 1_usize ) , ( 1 , 5 ) ] . as_slice ( )
951+ [
952+ ( 1_usize , 1_usize , MAX_SATURATING_TREND_TAU_COUNT ) ,
953+ ( 1 , 5 , MAX_SATURATING_TREND_TAU_COUNT ) ,
954+ ]
955+ . as_slice ( )
956+ } else if model. is_saturating_trend_basis ( ) {
957+ [
958+ ( 1_usize , 1_usize , MIN_SATURATING_TREND_TAU_COUNT ) ,
959+ ( 1 , 1 , MAX_SATURATING_TREND_TAU_COUNT ) ,
960+ ]
961+ . as_slice ( )
863962 } else {
864- [ ( 1_usize , 1_usize ) ] . as_slice ( )
963+ [ ( 1_usize , 1_usize , MAX_SATURATING_TREND_TAU_COUNT ) ] . as_slice ( )
865964 } ;
866- for & ( polynomial_degree, rational_degree) in degree_pairs {
965+ for & ( polynomial_degree, rational_degree, tau_count ) in degree_triplets {
867966 let degree = if model. is_polynomial ( ) {
868967 polynomial_degree
869968 } else if model. is_rational ( ) {
870969 rational_degree
970+ } else if model. is_saturating_trend_basis ( ) {
971+ tau_count
871972 } else {
872973 1
873974 } ;
@@ -876,6 +977,8 @@ mod tests {
876977 model,
877978 polynomial_degree,
878979 rational_degree,
980+ tau_count,
981+ None ,
879982 OptimizationLossMetric :: Mse ,
880983 ) ;
881984 for section in & formula. sections {
@@ -907,18 +1010,34 @@ mod tests {
9071010 #[ test]
9081011 fn every_model_plain_text_stays_readable ( ) {
9091012 for model in ModelChoice :: ALL {
910- let degree_pairs = if model. is_polynomial ( ) {
911- [ ( 1_usize , 1_usize ) , ( 9 , 1 ) ] . as_slice ( )
1013+ let degree_triplets = if model. is_polynomial ( ) {
1014+ [
1015+ ( 1_usize , 1_usize , MAX_SATURATING_TREND_TAU_COUNT ) ,
1016+ ( 9 , 1 , MAX_SATURATING_TREND_TAU_COUNT ) ,
1017+ ]
1018+ . as_slice ( )
9121019 } else if model. is_rational ( ) {
913- [ ( 1_usize , 1_usize ) , ( 1 , 5 ) ] . as_slice ( )
1020+ [
1021+ ( 1_usize , 1_usize , MAX_SATURATING_TREND_TAU_COUNT ) ,
1022+ ( 1 , 5 , MAX_SATURATING_TREND_TAU_COUNT ) ,
1023+ ]
1024+ . as_slice ( )
1025+ } else if model. is_saturating_trend_basis ( ) {
1026+ [
1027+ ( 1_usize , 1_usize , MIN_SATURATING_TREND_TAU_COUNT ) ,
1028+ ( 1 , 1 , MAX_SATURATING_TREND_TAU_COUNT ) ,
1029+ ]
1030+ . as_slice ( )
9141031 } else {
915- [ ( 1_usize , 1_usize ) ] . as_slice ( )
1032+ [ ( 1_usize , 1_usize , MAX_SATURATING_TREND_TAU_COUNT ) ] . as_slice ( )
9161033 } ;
917- for & ( polynomial_degree, rational_degree) in degree_pairs {
1034+ for & ( polynomial_degree, rational_degree, tau_count ) in degree_triplets {
9181035 let degree = if model. is_polynomial ( ) {
9191036 polynomial_degree
9201037 } else if model. is_rational ( ) {
9211038 rational_degree
1039+ } else if model. is_saturating_trend_basis ( ) {
1040+ tau_count
9221041 } else {
9231042 1
9241043 } ;
@@ -927,6 +1046,8 @@ mod tests {
9271046 model,
9281047 polynomial_degree,
9291048 rational_degree,
1049+ tau_count,
1050+ None ,
9301051 OptimizationLossMetric :: Mse ,
9311052 ) ;
9321053 let plain_text = formula. model_plain_text . trim ( ) ;
@@ -958,8 +1079,15 @@ mod tests {
9581079 #[ test]
9591080 fn every_optimization_metric_formula_renders_as_svg ( ) {
9601081 for metric in OptimizationLossMetric :: ALL {
961- let formula =
962- model_formula_info ( UiLanguage :: English , ModelChoice :: Polynomial , 3 , 1 , metric) ;
1082+ let formula = model_formula_info (
1083+ UiLanguage :: English ,
1084+ ModelChoice :: Polynomial ,
1085+ 3 ,
1086+ 1 ,
1087+ MAX_SATURATING_TREND_TAU_COUNT ,
1088+ None ,
1089+ metric,
1090+ ) ;
9631091 let metric_section = formula
9641092 . sections
9651093 . iter ( )
@@ -983,6 +1111,8 @@ mod tests {
9831111 ModelChoice :: Polynomial ,
9841112 3 ,
9851113 1 ,
1114+ MAX_SATURATING_TREND_TAU_COUNT ,
1115+ None ,
9861116 OptimizationLossMetric :: SoftL1 ,
9871117 ) ;
9881118 let titles: Vec < & str > = formula
@@ -1013,6 +1143,8 @@ mod tests {
10131143 ModelChoice :: Polynomial ,
10141144 3 ,
10151145 1 ,
1146+ MAX_SATURATING_TREND_TAU_COUNT ,
1147+ None ,
10161148 OptimizationLossMetric :: Mse ,
10171149 ) ;
10181150 let metric_section = formula
@@ -1034,8 +1166,15 @@ mod tests {
10341166 #[ test]
10351167 fn optimization_metric_reference_omits_loss_derivative_formulas ( ) {
10361168 for metric in OptimizationLossMetric :: ALL {
1037- let formula =
1038- model_formula_info ( UiLanguage :: English , ModelChoice :: Polynomial , 3 , 1 , metric) ;
1169+ let formula = model_formula_info (
1170+ UiLanguage :: English ,
1171+ ModelChoice :: Polynomial ,
1172+ 3 ,
1173+ 1 ,
1174+ MAX_SATURATING_TREND_TAU_COUNT ,
1175+ None ,
1176+ metric,
1177+ ) ;
10391178 let metric_section = formula
10401179 . sections
10411180 . iter ( )
@@ -1076,6 +1215,8 @@ mod tests {
10761215 ModelChoice :: PseudoVoigt ,
10771216 1 ,
10781217 1 ,
1218+ MAX_SATURATING_TREND_TAU_COUNT ,
1219+ None ,
10791220 OptimizationLossMetric :: Mse ,
10801221 ) ;
10811222 let model_section = formula
@@ -1096,6 +1237,8 @@ mod tests {
10961237 ModelChoice :: PseudoVoigt ,
10971238 1 ,
10981239 1 ,
1240+ MAX_SATURATING_TREND_TAU_COUNT ,
1241+ None ,
10991242 OptimizationLossMetric :: Mse ,
11001243 ) ;
11011244
0 commit comments