From b9d6f571e91ea7cee574d6e1257d37065ac72731 Mon Sep 17 00:00:00 2001 From: jaideeppyne Date: Fri, 28 Aug 2026 07:29:43 +0530 Subject: [PATCH 1/3] fix: Triangular::pdf returns NaN when mode equals min `Triangular::new` accepts `mode == min` (a right triangle), and `test_create` already covers it with `create_ok(1.0, 2.0, 1.0)`. But `pdf` evaluates the rising edge as 2 * (x - min) / ((max - min) * (mode - min)) and at `x == min == mode` both the numerator and the `(mode - min)` factor are zero, so the result is 0 / 0 = NaN. `ln_pdf` delegates to `pdf` and returns NaN as well. let d = Triangular::new(0.0, 1.0, 0.0).unwrap(); d.pdf(0.0); // NaN, expected 2.0 d.ln_pdf(0.0); // NaN, expected ln(2) The mirrored `mode == max` case is already correct: `pdf` takes the same first branch and returns `2 / (max - min)`, the height of the triangle. Handle the degenerate rising edge so both endpoints agree. `x == min == mode` is the only `x` that can reach that branch when `mode == min`, so no other input changes value. --- src/distribution/triangular.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/distribution/triangular.rs b/src/distribution/triangular.rs index 8f926116..eb440388 100644 --- a/src/distribution/triangular.rs +++ b/src/distribution/triangular.rs @@ -388,12 +388,24 @@ impl Continuous for Triangular { /// 0 /// } /// ``` + /// + /// When `mode == min` the second branch is degenerate and reduces to `0 / 0`; + /// the density at that single point is the height of the triangle, + /// `2 / (max - min)`. fn pdf(&self, x: f64) -> f64 { let a = self.min; let b = self.max; let c = self.mode; if a <= x && x <= c { - 2.0 * (x - a) / ((b - a) * (c - a)) + if c == a { + // The only `x` reaching this branch is `x == min == mode`, where the + // rising edge is degenerate and the expression below is 0 / 0. The + // density there is the height of the triangle, matching the value the + // falling edge already returns for the mirrored `mode == max` case. + 2.0 / (b - a) + } else { + 2.0 * (x - a) / ((b - a) * (c - a)) + } } else if c < x && x <= b { 2.0 * (b - x) / ((b - a) * (b - c)) } else { @@ -547,6 +559,12 @@ mod tests { test_exact(-5.0, -3.0, -4.0, 0.5, pdf(-4.5)); test_exact(-5.0, -3.0, -4.0, 1.0, pdf(-4.0)); test_exact(-5.0, -3.0, -4.0, 0.5, pdf(-3.5)); + // mode == min: the rising edge is degenerate at x == min, the density there + // is the height of the triangle + test_exact(0.0, 1.0, 0.0, 2.0, pdf(0.0)); + test_exact(-2.0, 5.0, -2.0, 2.0 / 7.0, pdf(-2.0)); + // mirrored case, mode == max + test_exact(0.0, 1.0, 1.0, 2.0, pdf(1.0)); } #[test] @@ -567,6 +585,8 @@ mod tests { test_exact(-5.0, -3.0, -4.0, 0.5f64.ln(), ln_pdf(-4.5)); test_exact(-5.0, -3.0, -4.0, 0.0, ln_pdf(-4.0)); test_exact(-5.0, -3.0, -4.0, 0.5f64.ln(), ln_pdf(-3.5)); + test_exact(0.0, 1.0, 0.0, 2f64.ln(), ln_pdf(0.0)); + test_exact(-2.0, 5.0, -2.0, (2.0f64 / 7.0).ln(), ln_pdf(-2.0)); } #[test] @@ -640,5 +660,7 @@ mod tests { fn test_continuous() { density_util::check_continuous_distribution(&create_ok(-5.0, 5.0, 0.0), -5.0, 5.0); density_util::check_continuous_distribution(&create_ok(-15.0, -2.0, -3.0), -15.0, -2.0); + density_util::check_continuous_distribution(&create_ok(-5.0, 5.0, -5.0), -5.0, 5.0); + density_util::check_continuous_distribution(&create_ok(-5.0, 5.0, 5.0), -5.0, 5.0); } } From 70402ba483a4d104e9479bb627f6c35cba8db3b6 Mon Sep 17 00:00:00 2001 From: jaideeppyne Date: Fri, 28 Aug 2026 16:05:17 +0530 Subject: [PATCH 2/3] Drop the explanatory comments and split out the endpoint tests The doc formula already states the mode == min case, and the degenerate edge assertions now live in their own tests instead of behind a comment. --- src/distribution/triangular.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/distribution/triangular.rs b/src/distribution/triangular.rs index eb440388..47a3863b 100644 --- a/src/distribution/triangular.rs +++ b/src/distribution/triangular.rs @@ -398,10 +398,6 @@ impl Continuous for Triangular { let c = self.mode; if a <= x && x <= c { if c == a { - // The only `x` reaching this branch is `x == min == mode`, where the - // rising edge is degenerate and the expression below is 0 / 0. The - // density there is the height of the triangle, matching the value the - // falling edge already returns for the mirrored `mode == max` case. 2.0 / (b - a) } else { 2.0 * (x - a) / ((b - a) * (c - a)) @@ -559,12 +555,15 @@ mod tests { test_exact(-5.0, -3.0, -4.0, 0.5, pdf(-4.5)); test_exact(-5.0, -3.0, -4.0, 1.0, pdf(-4.0)); test_exact(-5.0, -3.0, -4.0, 0.5, pdf(-3.5)); - // mode == min: the rising edge is degenerate at x == min, the density there - // is the height of the triangle + } + + #[test] + fn test_pdf_mode_at_an_endpoint() { + let pdf = |arg: f64| move |x: Triangular| x.pdf(arg); test_exact(0.0, 1.0, 0.0, 2.0, pdf(0.0)); test_exact(-2.0, 5.0, -2.0, 2.0 / 7.0, pdf(-2.0)); - // mirrored case, mode == max test_exact(0.0, 1.0, 1.0, 2.0, pdf(1.0)); + test_exact(-2.0, 5.0, 5.0, 2.0 / 7.0, pdf(5.0)); } #[test] @@ -585,8 +584,14 @@ mod tests { test_exact(-5.0, -3.0, -4.0, 0.5f64.ln(), ln_pdf(-4.5)); test_exact(-5.0, -3.0, -4.0, 0.0, ln_pdf(-4.0)); test_exact(-5.0, -3.0, -4.0, 0.5f64.ln(), ln_pdf(-3.5)); + } + + #[test] + fn test_ln_pdf_mode_at_an_endpoint() { + let ln_pdf = |arg: f64| move |x: Triangular| x.ln_pdf(arg); test_exact(0.0, 1.0, 0.0, 2f64.ln(), ln_pdf(0.0)); test_exact(-2.0, 5.0, -2.0, (2.0f64 / 7.0).ln(), ln_pdf(-2.0)); + test_exact(0.0, 1.0, 1.0, 2f64.ln(), ln_pdf(1.0)); } #[test] From 211756df46a01d2d0c588bdc764ec795c5548f25 Mon Sep 17 00:00:00 2001 From: jaideeppyne Date: Fri, 28 Aug 2026 19:35:20 +0530 Subject: [PATCH 3/3] Cut the doc note too --- src/distribution/triangular.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/distribution/triangular.rs b/src/distribution/triangular.rs index 47a3863b..c37a9ac1 100644 --- a/src/distribution/triangular.rs +++ b/src/distribution/triangular.rs @@ -388,10 +388,6 @@ impl Continuous for Triangular { /// 0 /// } /// ``` - /// - /// When `mode == min` the second branch is degenerate and reduces to `0 / 0`; - /// the density at that single point is the height of the triangle, - /// `2 / (max - min)`. fn pdf(&self, x: f64) -> f64 { let a = self.min; let b = self.max;