1use prospicio_core::{Error, Result};
12
13use crate::distribution::check_probability;
14
15pub fn var_sorted(sorted: &[f64], p: f64) -> Result<f64> {
30 check_probability(p)?;
31 debug_assert!(is_sorted(sorted), "draws must be sorted ascending");
32 Ok(sorted[rank(sorted.len(), p) - 1])
33}
34
35pub fn tvar_sorted(sorted: &[f64], p: f64) -> Result<f64> {
53 check_probability(p)?;
54 debug_assert!(is_sorted(sorted), "draws must be sorted ascending");
55 let n = sorted.len();
56 if p == 1.0 {
57 return Ok(sorted[n - 1]);
58 }
59 let k = rank(n, p);
60 let nf = n as f64;
61 let partial = (k as f64 / nf - p) * sorted[k - 1];
63 let tail: f64 = sorted[k..].iter().sum::<f64>() / nf;
64 Ok((partial + tail) / (1.0 - p))
65}
66
67pub fn entropic(draws: &[f64], theta: f64) -> Result<f64> {
81 check_draws(draws)?;
82 if !(theta.is_finite() && theta > 0.0) {
83 return Err(Error::InvalidParameter {
84 name: "theta",
85 value: theta,
86 reason: "must be finite and positive",
87 });
88 }
89 let m = draws.iter().copied().fold(f64::NEG_INFINITY, f64::max);
90 let mean_exp = draws.iter().map(|x| (theta * (x - m)).exp()).sum::<f64>() / draws.len() as f64;
91 Ok(m + mean_exp.ln() / theta)
92}
93
94pub fn esscher(draws: &[f64], h: f64) -> Result<f64> {
105 check_draws(draws)?;
106 if !h.is_finite() {
107 return Err(Error::InvalidParameter {
108 name: "h",
109 value: h,
110 reason: "must be finite",
111 });
112 }
113 let w = esscher_weights(draws, h);
114 Ok(w.iter().zip(draws).map(|(w, x)| w * x).sum())
115}
116
117pub(crate) fn esscher_weights(draws: &[f64], h: f64) -> Vec<f64> {
119 let m = draws
120 .iter()
121 .map(|x| h * x)
122 .fold(f64::NEG_INFINITY, f64::max);
123 let w: Vec<f64> = draws.iter().map(|x| (h * x - m).exp()).collect();
124 let total: f64 = w.iter().sum();
125 w.into_iter().map(|v| v / total).collect()
126}
127
128fn check_draws(draws: &[f64]) -> Result<()> {
129 if draws.is_empty() {
130 return Err(Error::InvalidParameter {
131 name: "draws",
132 value: 0.0,
133 reason: "must not be empty",
134 });
135 }
136 Ok(())
137}
138
139fn rank(n: usize, p: f64) -> usize {
142 let nf = n as f64;
143 let mut k = ((p * nf).ceil() as usize).clamp(1, n);
144 while k > 1 && (k - 1) as f64 / nf >= p {
145 k -= 1;
146 }
147 while k < n && (k as f64 / nf) < p {
148 k += 1;
149 }
150 k
151}
152
153fn is_sorted(x: &[f64]) -> bool {
154 x.windows(2).all(|w| w[0] <= w[1])
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
164 fn exponential_utility_measures_match_the_normal() {
165 let n = 200_000;
166 let (mu, sigma) = (3.0, 2.0);
167 let x: Vec<f64> = (0..n)
168 .map(|i| {
169 mu + sigma * prospicio_math::special::norm_quantile((i as f64 + 0.5) / n as f64)
170 })
171 .collect();
172 let theta = 0.4;
173 assert!((entropic(&x, theta).unwrap() - (mu + theta * sigma * sigma / 2.0)).abs() < 2e-3);
174 let h = 0.3;
175 assert!((esscher(&x, h).unwrap() - (mu + h * sigma * sigma)).abs() < 2e-3);
176 assert!((esscher(&x, 0.0).unwrap() - mu).abs() < 1e-9);
177 assert!((entropic(&[1e6, 1e6], 1.0).unwrap() - 1e6).abs() < 1e-6);
179 assert!(entropic(&x, 0.0).is_err() && esscher(&[], 1.0).is_err());
180 }
181 use prospicio_core::Error;
182
183 const X: [f64; 5] = [10.0, 20.0, 30.0, 40.0, 50.0];
184
185 #[test]
186 fn var_steps_at_multiples_of_one_over_n() {
187 assert_eq!(var_sorted(&X, 0.0), Ok(10.0));
188 assert_eq!(var_sorted(&X, 0.2), Ok(10.0));
189 assert_eq!(var_sorted(&X, 0.2000001), Ok(20.0));
190 assert_eq!(var_sorted(&X, 0.6), Ok(30.0));
191 assert_eq!(var_sorted(&X, 1.0), Ok(50.0));
192 }
193
194 #[test]
195 fn rank_is_exact_where_p_times_n_rounds_up() {
196 assert_eq!(rank(20, 0.95), 19);
198 assert_eq!(rank(10, 0.7), 7);
200 assert_eq!(rank(3, 1.0 / 3.0), 1);
201 }
202
203 fn assert_close(got: f64, want: f64) {
204 assert!((got - want).abs() <= 1e-12 * want.abs(), "{got} != {want}");
205 }
206
207 #[test]
208 fn tvar_edges_and_tail_means() {
209 assert_close(tvar_sorted(&X, 0.0).unwrap(), 30.0);
210 assert_close(tvar_sorted(&X, 0.6).unwrap(), 45.0);
211 assert_close(tvar_sorted(&X, 0.8).unwrap(), 50.0);
212 assert_eq!(tvar_sorted(&X, 1.0), Ok(50.0));
213 assert_close(tvar_sorted(&X, 0.7).unwrap(), 14.0 / 0.3);
215 }
216
217 #[test]
218 fn tvar_is_at_least_var_and_continuous() {
219 let mut prev = tvar_sorted(&X, 0.0).unwrap();
220 for i in 1..=1000 {
221 let p = i as f64 / 1000.0;
222 let t = tvar_sorted(&X, p).unwrap();
223 assert!(t >= var_sorted(&X, p).unwrap() - 1e-12);
224 assert!(t >= prev - 1e-12, "not monotone at {p}");
225 assert!(t - prev < 0.6, "jump at {p}");
226 prev = t;
227 }
228 }
229
230 #[test]
231 fn ties_count_as_one_atom() {
232 let x = [1.0, 5.0, 5.0, 5.0];
233 assert_eq!(var_sorted(&x, 0.3), Ok(5.0));
234 assert_close(tvar_sorted(&x, 0.1).unwrap(), (0.15 + 3.75) / 0.9);
236 }
237
238 #[test]
239 fn rejects_bad_probability() {
240 assert_eq!(var_sorted(&X, -0.1), Err(Error::InvalidProbability(-0.1)));
241 assert!(tvar_sorted(&X, f64::NAN).is_err());
242 }
243}