Skip to content

Commit cdce33d

Browse files
committed
added Rational 1/1, 2/2, Emg and PseudoVoigt model curve
1 parent 5a19edb commit cdce33d

11 files changed

Lines changed: 924 additions & 49 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "curve-fit"
3-
version = "0.1.6"
3+
version = "0.1.7"
44
edition = "2024"
55
authors = ["hexqnt <hexqntlab@gmail.com>"]
66
license = "Apache-2.0 OR MIT"

src/app.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,14 +231,18 @@ enum ModelChoice {
231231
Softplus,
232232
Power,
233233
Gaussian,
234+
Rational11,
235+
Rational22,
236+
Emg,
237+
PseudoVoigt,
234238
LinearSpline,
235239
MonotoneCubicSpline,
236240
NaturalCubicSpline,
237241
AkimaSpline,
238242
}
239243

240244
impl ModelChoice {
241-
const ALL: [Self; 25] = [
245+
const ALL: [Self; 29] = [
242246
Self::Polynomial,
243247
Self::Arrhenius,
244248
Self::Inverse,
@@ -260,6 +264,10 @@ impl ModelChoice {
260264
Self::Softplus,
261265
Self::Power,
262266
Self::Gaussian,
267+
Self::Rational11,
268+
Self::Rational22,
269+
Self::Emg,
270+
Self::PseudoVoigt,
263271
Self::LinearSpline,
264272
Self::MonotoneCubicSpline,
265273
Self::NaturalCubicSpline,
@@ -304,6 +312,10 @@ impl ResolvedModel {
304312
ModelChoice::Softplus => Self::Parametric(CurveFamily::Softplus),
305313
ModelChoice::Power => Self::Parametric(CurveFamily::Power),
306314
ModelChoice::Gaussian => Self::Parametric(CurveFamily::Gaussian),
315+
ModelChoice::Rational11 => Self::Parametric(CurveFamily::Rational11),
316+
ModelChoice::Rational22 => Self::Parametric(CurveFamily::Rational22),
317+
ModelChoice::Emg => Self::Parametric(CurveFamily::Emg),
318+
ModelChoice::PseudoVoigt => Self::Parametric(CurveFamily::PseudoVoigt),
307319
ModelChoice::LinearSpline => Self::LinearSpline,
308320
ModelChoice::MonotoneCubicSpline => Self::MonotoneCubicSpline,
309321
ModelChoice::NaturalCubicSpline => Self::NaturalCubicSpline,
@@ -370,7 +382,10 @@ fn model_group(model: ModelChoice) -> ModelGroup {
370382
| ModelChoice::HyperbolicTangent
371383
| ModelChoice::ArctangentStep
372384
| ModelChoice::Softplus => ModelGroup::ParametricSigmoid,
373-
ModelChoice::Lorentzian | ModelChoice::Gaussian => ModelGroup::ParametricPeak,
385+
ModelChoice::Lorentzian
386+
| ModelChoice::Gaussian
387+
| ModelChoice::Emg
388+
| ModelChoice::PseudoVoigt => ModelGroup::ParametricPeak,
374389
ModelChoice::LinearSpline
375390
| ModelChoice::MonotoneCubicSpline
376391
| ModelChoice::NaturalCubicSpline
@@ -385,7 +400,9 @@ fn model_group(model: ModelChoice) -> ModelGroup {
385400
| ModelChoice::ExponentialLinear
386401
| ModelChoice::ExponentialHalfLife
387402
| ModelChoice::FallingExponential
388-
| ModelChoice::Power => ModelGroup::ParametricGeneral,
403+
| ModelChoice::Power
404+
| ModelChoice::Rational11
405+
| ModelChoice::Rational22 => ModelGroup::ParametricGeneral,
389406
}
390407
}
391408

src/app/formula.rs

Lines changed: 180 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ fn model_formula_full(model: ModelChoice, polynomial_degree: usize) -> String {
1515
r"y = a_{1}·exp(-k_{1}·x) + a_{2}·exp(-k_{2}·x) + c".to_string()
1616
}
1717
ModelChoice::DampedSinusoid => r"y = A·exp(-k·x)·sin(\omega·x + \phi) + c".to_string(),
18-
ModelChoice::Lorentzian => r"y = C + \frac{A}{1 + (\frac{x - x_0}{gamma})^{2}}".to_string(),
18+
ModelChoice::Lorentzian => r"y = C + \frac{A}{1 + (\frac{x - x_0}{\gamma})^{2}}".to_string(),
1919
ModelChoice::NaturalLog => r"y = A·ln(\frac{x}{B})".to_string(),
2020
ModelChoice::FourPl => r"y = d + \frac{a - d}{1 + (\frac{x}{c})^{b}}".to_string(),
2121
ModelChoice::FivePl => r"y = d + \frac{a - d}{(1 + (\frac{x}{c})^{b})^{m}}".to_string(),
@@ -31,6 +31,19 @@ fn model_formula_full(model: ModelChoice, polynomial_degree: usize) -> String {
3131
ModelChoice::Softplus => r"y = a·ln(1 + exp(b·(x - c))) + d".to_string(),
3232
ModelChoice::Power => r"y = a·x^{b}".to_string(),
3333
ModelChoice::Gaussian => r"y = a·exp(-\frac{(x - b)^{2}}{2·c^{2}})".to_string(),
34+
ModelChoice::Rational11 => r"y = d + \frac{a·x + b}{1 + c·x}".to_string(),
35+
ModelChoice::Rational22 => {
36+
r"y = \frac{a·x^{2} + b·x + c}{1 + d·x + e·x^{2}}".to_string()
37+
}
38+
ModelChoice::Emg => {
39+
r"y = c + \frac{a}{2·\tau}·exp(\frac{\sigma^{2}}{2·\tau^{2}} - \frac{x - \mu}{\tau})·erfc(\frac{1}{\sqrt{2}}(\frac{\sigma}{|\tau|} - \frac{x - \mu}{\sigma}))".to_string()
40+
}
41+
ModelChoice::PseudoVoigt => {
42+
r"y = c + a·(\eta·G(x; x_0, \sigma) + (1-\eta)·L(x; x_0, \gamma)), \eta = sigmoid(\eta_{raw})
43+
G(x; x_0, \sigma) = exp(-\frac{(x - x_0)^{2}}{2·\sigma^{2}})
44+
L(x; x_0, \gamma) = \frac{1}{1 + (\frac{x - x_0}{\gamma})^{2}}"
45+
.to_string()
46+
}
3447
ModelChoice::LinearSpline => {
3548
r"y(x) = y_{i} + \frac{y_{i+1} - y_{i}}{x_{i+1} - x_{i}}·(x - x_{i})".to_string()
3649
}
@@ -127,6 +140,18 @@ fn model_ml_note(language: UiLanguage, model: ModelChoice) -> &'static str {
127140
(UiLanguage::English, ModelChoice::ExponentialLinear) => {
128141
"Exponential trend with linear drift background."
129142
}
143+
(UiLanguage::English, ModelChoice::Rational11) => {
144+
"Compact rational model with one linear pole term."
145+
}
146+
(UiLanguage::English, ModelChoice::Rational22) => {
147+
"Flexible rational model with quadratic numerator/denominator."
148+
}
149+
(UiLanguage::English, ModelChoice::Emg) => {
150+
"Asymmetric peak (EMG); signed tau controls left/right tail."
151+
}
152+
(UiLanguage::English, ModelChoice::PseudoVoigt) => {
153+
"Mixture of Gaussian and Lorentzian peaks with learnable blend."
154+
}
130155
(UiLanguage::English, ModelChoice::HyperbolicTangent) => {
131156
"Smooth S-curve transition with bounded tails."
132157
}
@@ -179,6 +204,18 @@ fn model_ml_note(language: UiLanguage, model: ModelChoice) -> &'static str {
179204
(UiLanguage::Russian, ModelChoice::ExponentialLinear) => {
180205
"Экспоненциальный тренд с линейным дрейфом фона."
181206
}
207+
(UiLanguage::Russian, ModelChoice::Rational11) => {
208+
"Компактная рациональная модель с линейным полюсом."
209+
}
210+
(UiLanguage::Russian, ModelChoice::Rational22) => {
211+
"Гибкая рациональная модель с квадратами в числителе и знаменателе."
212+
}
213+
(UiLanguage::Russian, ModelChoice::Emg) => {
214+
"Асимметричный пик EMG; знак tau задаёт левый или правый хвост."
215+
}
216+
(UiLanguage::Russian, ModelChoice::PseudoVoigt) => {
217+
"Смесь гауссового и лоренцевого пиков с обучаемой долей."
218+
}
182219
(UiLanguage::Russian, ModelChoice::HyperbolicTangent) => {
183220
"Гладкий S-переход с ограниченными хвостами."
184221
}
@@ -240,23 +277,44 @@ pub(super) fn formula_plain_text(formula: &str) -> String {
240277
/// чтобы не тянуть тяжёлые зависимости ради ограниченного подмножества LaTeX.
241278
#[cfg(not(target_arch = "wasm32"))]
242279
pub(super) fn formula_svg_bytes(formula: &str, dark_mode: bool) -> Vec<u8> {
243-
let spans = parse_formula_spans(formula);
244-
let visible_chars: usize = spans.iter().map(|span| span.text.chars().count()).sum();
245-
let width = ((visible_chars.max(24) as u32) * 14 + 48).clamp(380, 2100);
246-
let height = 68_u32;
280+
let lines = formula.lines().collect::<Vec<_>>();
281+
let line_count = lines.len().max(1);
282+
let line_spans = lines
283+
.iter()
284+
.map(|line| parse_formula_spans(line))
285+
.collect::<Vec<_>>();
286+
let max_visible_chars = line_spans
287+
.iter()
288+
.map(|spans| {
289+
spans
290+
.iter()
291+
.map(|span| span.text.chars().count())
292+
.sum::<usize>()
293+
})
294+
.max()
295+
.unwrap_or(24);
296+
let width = ((max_visible_chars.max(24) as u32) * 14 + 48).clamp(380, 2100);
297+
let height = ((line_count as u32) * 30 + 22).clamp(68, 720);
247298
let rect_width = width - 2;
248299
let rect_height = height - 2;
249300
let (background, border, text) = if dark_mode {
250301
("#0f172a", "#334155", "#f8fafc")
251302
} else {
252303
("#ffffff", "#cbd5e1", "#111827")
253304
};
254-
let tspan_markup = formula_spans_to_svg(&spans);
305+
let mut text_markup = String::new();
306+
for (index, spans) in line_spans.iter().enumerate() {
307+
let tspan_markup = formula_spans_to_svg(spans);
308+
let y = 32 + (index as u32) * 30;
309+
text_markup.push_str(&format!(
310+
r#"<text x="16" y="{y}" font-family="Cambria Math, STIX Two Math, DejaVu Serif, serif" font-size="24" fill="{text}">{tspan_markup}</text>"#
311+
));
312+
}
255313

256314
format!(
257315
r#"<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}">
258316
<rect x="1" y="1" width="{rect_width}" height="{rect_height}" rx="10" fill="{background}" stroke="{border}" stroke-width="1.4"/>
259-
<text x="16" y="44" font-family="Cambria Math, STIX Two Math, DejaVu Serif, serif" font-size="24" fill="{text}">{tspan_markup}</text>
317+
{text_markup}
260318
</svg>"#
261319
)
262320
.into_bytes()
@@ -292,7 +350,7 @@ fn parse_formula_spans(formula: &str) -> Vec<FormulaSpan> {
292350
if !normal.is_empty() {
293351
spans.push(FormulaSpan {
294352
kind: FormulaSpanKind::Normal,
295-
text: std::mem::take(&mut normal),
353+
text: latex_group_to_text(&std::mem::take(&mut normal)),
296354
});
297355
}
298356
let numerator = read_braced_group(&mut chars);
@@ -323,7 +381,7 @@ fn parse_formula_spans(formula: &str) -> Vec<FormulaSpan> {
323381
if !normal.is_empty() {
324382
spans.push(FormulaSpan {
325383
kind: FormulaSpanKind::Normal,
326-
text: std::mem::take(&mut normal),
384+
text: latex_group_to_text(&std::mem::take(&mut normal)),
327385
});
328386
}
329387
chars.next();
@@ -334,7 +392,7 @@ fn parse_formula_spans(formula: &str) -> Vec<FormulaSpan> {
334392
} else {
335393
FormulaSpanKind::Subscript
336394
},
337-
text: content,
395+
text: latex_group_to_text(&content),
338396
});
339397
} else {
340398
normal.push(ch);
@@ -344,7 +402,7 @@ fn parse_formula_spans(formula: &str) -> Vec<FormulaSpan> {
344402
if !normal.is_empty() {
345403
spans.push(FormulaSpan {
346404
kind: FormulaSpanKind::Normal,
347-
text: normal,
405+
text: latex_group_to_text(&normal),
348406
});
349407
}
350408
spans
@@ -386,38 +444,125 @@ fn read_braced_group(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> St
386444
content
387445
}
388446

447+
fn read_latex_command(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> String {
448+
let mut command = String::new();
449+
while let Some(ch) = chars.peek().copied() {
450+
if ch.is_ascii_alphabetic() {
451+
command.push(ch);
452+
chars.next();
453+
} else {
454+
break;
455+
}
456+
}
457+
command
458+
}
459+
460+
fn latex_symbol(command: &str) -> Option<&'static str> {
461+
match command {
462+
"alpha" => Some("α"),
463+
"beta" => Some("β"),
464+
"gamma" => Some("γ"),
465+
"delta" => Some("δ"),
466+
"epsilon" => Some("ε"),
467+
"zeta" => Some("ζ"),
468+
"eta" => Some("η"),
469+
"theta" => Some("θ"),
470+
"iota" => Some("ι"),
471+
"kappa" => Some("κ"),
472+
"lambda" => Some("λ"),
473+
"mu" => Some("μ"),
474+
"nu" => Some("ν"),
475+
"xi" => Some("ξ"),
476+
"pi" => Some("π"),
477+
"rho" => Some("ρ"),
478+
"sigma" => Some("σ"),
479+
"tau" => Some("τ"),
480+
"upsilon" => Some("υ"),
481+
"phi" => Some("φ"),
482+
"chi" => Some("χ"),
483+
"psi" => Some("ψ"),
484+
"omega" => Some("ω"),
485+
"Gamma" => Some("Γ"),
486+
"Delta" => Some("Δ"),
487+
"Theta" => Some("Θ"),
488+
"Lambda" => Some("Λ"),
489+
"Xi" => Some("Ξ"),
490+
"Pi" => Some("Π"),
491+
"Sigma" => Some("Σ"),
492+
"Upsilon" => Some("Υ"),
493+
"Phi" => Some("Φ"),
494+
"Psi" => Some("Ψ"),
495+
"Omega" => Some("Ω"),
496+
"cdot" => Some("·"),
497+
"times" => Some("×"),
498+
"pm" => Some("±"),
499+
"leq" => Some("≤"),
500+
"geq" => Some("≥"),
501+
"infty" => Some("∞"),
502+
_ => None,
503+
}
504+
}
505+
389506
fn latex_group_to_text(text: &str) -> String {
390507
let mut output = String::new();
391508
let mut chars = text.chars().peekable();
392509
while let Some(ch) = chars.next() {
393510
if ch == '\\' {
394-
if try_consume_keyword(&mut chars, "frac") && matches!(chars.next(), Some('{')) {
395-
let numerator = read_braced_group(&mut chars);
396-
if matches!(chars.next(), Some('{')) {
397-
let denominator = read_braced_group(&mut chars);
398-
output.push('(');
399-
output.push_str(&latex_group_to_text(&numerator));
400-
output.push(')');
401-
output.push('∕');
402-
output.push('(');
403-
output.push_str(&latex_group_to_text(&denominator));
404-
output.push(')');
405-
continue;
406-
}
407-
output.push_str("\\frac{");
408-
output.push_str(&numerator);
409-
continue;
410-
}
411-
if try_consume_keyword(&mut chars, "quad") {
412-
output.push(' ');
511+
let command = read_latex_command(&mut chars);
512+
if command.is_empty() {
513+
output.push(ch);
413514
continue;
414515
}
415-
if try_consume_keyword(&mut chars, "text") && matches!(chars.next(), Some('{')) {
416-
let content = read_braced_group(&mut chars);
417-
output.push_str(&latex_group_to_text(&content));
418-
continue;
516+
match command.as_str() {
517+
"frac" => {
518+
if matches!(chars.next(), Some('{')) {
519+
let numerator = read_braced_group(&mut chars);
520+
if matches!(chars.next(), Some('{')) {
521+
let denominator = read_braced_group(&mut chars);
522+
output.push('(');
523+
output.push_str(&latex_group_to_text(&numerator));
524+
output.push(')');
525+
output.push('∕');
526+
output.push('(');
527+
output.push_str(&latex_group_to_text(&denominator));
528+
output.push(')');
529+
continue;
530+
}
531+
output.push_str("\\frac{");
532+
output.push_str(&numerator);
533+
continue;
534+
}
535+
output.push_str("\\frac");
536+
}
537+
"quad" => output.push(' '),
538+
"text" => {
539+
if matches!(chars.next(), Some('{')) {
540+
let content = read_braced_group(&mut chars);
541+
output.push_str(&latex_group_to_text(&content));
542+
} else {
543+
output.push_str("\\text");
544+
}
545+
}
546+
"sqrt" => {
547+
if matches!(chars.next(), Some('{')) {
548+
let radicand = read_braced_group(&mut chars);
549+
output.push('√');
550+
output.push('(');
551+
output.push_str(&latex_group_to_text(&radicand));
552+
output.push(')');
553+
} else {
554+
output.push('√');
555+
}
556+
}
557+
_ => {
558+
if let Some(symbol) = latex_symbol(&command) {
559+
output.push_str(symbol);
560+
} else {
561+
output.push('\\');
562+
output.push_str(&command);
563+
}
564+
}
419565
}
420-
output.push(ch);
421566
continue;
422567
}
423568
if (ch == '^' || ch == '_') && matches!(chars.peek(), Some('{')) {

0 commit comments

Comments
 (0)