1pub const MAX_POPULATION: usize = 256;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31#[repr(u8)]
32pub enum WelfareError {
33 InvalidInput = 0,
36 InsufficientData = 1,
38 NonFinite = 2,
41 BufferTooSmall = 3,
43}
44
45impl WelfareError {
46 #[inline]
48 pub fn is_caller_error(self) -> bool {
49 matches!(
50 self,
51 WelfareError::InvalidInput
52 | WelfareError::BufferTooSmall
53 | WelfareError::InsufficientData
54 )
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq)]
65#[repr(C)]
66pub struct WelfareReport {
67 pub value: f64,
70 pub auxiliary: f64,
72 pub assumptions: u32,
74 pub diagnostics: u32,
77}
78
79pub const ASSUMPTION_FLOOR_CLAMPED: u32 = 1 << 0;
81pub const ASSUMPTION_WEIGHTED: u32 = 1 << 1;
83pub const ASSUMPTION_DEGENERATE: u32 = 1 << 2;
85pub const ASSUMPTION_LINE_ABOVE_MAX: u32 = 1 << 3;
87
88impl WelfareReport {
89 pub const fn clean(value: f64, auxiliary: f64) -> Self {
91 Self {
92 value,
93 auxiliary,
94 assumptions: 0,
95 diagnostics: 0,
96 }
97 }
98
99 pub const fn with_assumptions(value: f64, auxiliary: f64, assumptions: u32) -> Self {
101 Self {
102 value,
103 auxiliary,
104 assumptions,
105 diagnostics: 0,
106 }
107 }
108}
109
110#[inline]
115fn finite(x: f64) -> bool {
116 x.is_finite()
117}
118
119#[inline]
120fn finite_nonnegative(x: f64) -> bool {
121 x.is_finite() && x >= 0.0
122}
123
124#[inline]
125fn finite_strictly_positive(x: f64) -> bool {
126 x.is_finite() && x > 0.0
127}
128
129fn validate_population(incomes: &[f64]) -> Result<usize, WelfareError> {
132 if incomes.is_empty() {
133 return Err(WelfareError::InsufficientData);
134 }
135 if incomes.len() > MAX_POPULATION {
136 return Err(WelfareError::BufferTooSmall);
137 }
138 for &x in incomes {
139 if !finite_nonnegative(x) {
140 if !finite(x) {
141 return Err(WelfareError::NonFinite);
142 }
143 return Err(WelfareError::InvalidInput);
144 }
145 }
146 Ok(incomes.len())
147}
148
149fn validate_positive_population(incomes: &[f64]) -> Result<usize, WelfareError> {
152 if incomes.is_empty() {
153 return Err(WelfareError::InsufficientData);
154 }
155 if incomes.len() > MAX_POPULATION {
156 return Err(WelfareError::BufferTooSmall);
157 }
158 for &x in incomes {
159 if !finite(x) {
160 return Err(WelfareError::NonFinite);
161 }
162 if x <= 0.0 {
163 return Err(WelfareError::InvalidInput);
164 }
165 }
166 Ok(incomes.len())
167}
168
169fn copy_sort_ascending(incomes: &[f64]) -> Result<[f64; MAX_POPULATION], WelfareError> {
172 let n = validate_population(incomes)?;
173 let mut scratch = [0.0f64; MAX_POPULATION];
174 for (i, &x) in incomes.iter().enumerate() {
175 scratch[i] = x;
176 }
177 scratch[..n].sort_by(|a, b| a.total_cmp(b));
178 Ok(scratch)
179}
180
181pub fn gini_coefficient(incomes: &[f64]) -> Result<f64, WelfareError> {
197 let n = validate_population(incomes)?;
198 let mut sum: f64 = 0.0;
199 let mut total: f64 = 0.0;
200 for i in 0..n {
201 total += incomes[i];
202 for j in 0..n {
203 let d = incomes[i] - incomes[j];
204 sum += d.abs();
205 }
206 }
207 if total <= 0.0 {
208 return Err(WelfareError::InvalidInput);
210 }
211 let mean = total / n as f64;
212 let denom = 2.0 * (n as f64).powi(2) * mean;
213 Ok(sum / denom)
214}
215
216pub fn lorenz_curve_into(incomes: &[f64], out: &mut [f64]) -> Result<usize, WelfareError> {
227 let n = validate_population(incomes)?;
228 if out.len() < 2 * n {
229 return Err(WelfareError::BufferTooSmall);
230 }
231 let sorted = copy_sort_ascending(incomes)?;
232 let mut total = 0.0f64;
233 for i in 0..n {
234 total += sorted[i];
235 }
236 if total <= 0.0 {
237 return Err(WelfareError::InvalidInput);
238 }
239 let mut cum = 0.0f64;
240 for i in 0..n {
241 cum += sorted[i];
242 out[2 * i] = (i + 1) as f64 / n as f64;
243 out[2 * i + 1] = cum / total;
244 }
245 Ok(n)
246}
247
248pub fn atkinson_inequality(incomes: &[f64], epsilon: f64) -> Result<f64, WelfareError> {
260 let n = validate_positive_population(incomes)?;
261 if !finite(epsilon) || epsilon <= 0.0 {
262 return Err(WelfareError::InvalidInput);
263 }
264
265 let mut sum = 0.0f64;
266 for i in 0..n {
267 if incomes[i] <= 0.0 {
268 return Err(WelfareError::InvalidInput);
269 }
270 sum += incomes[i];
271 }
272 let arithmetic_mean = sum / n as f64;
273 if arithmetic_mean <= 0.0 {
274 return Err(WelfareError::InvalidInput);
275 }
276
277 if (epsilon - 1.0).abs() < f64::EPSILON {
278 let mut log_sum = 0.0f64;
280 for i in 0..n {
281 log_sum += incomes[i].ln();
282 }
283 let geo = (log_sum / n as f64).exp();
284 let a = 1.0 - (geo / arithmetic_mean);
285 return Ok(a.clamp(0.0, 1.0));
286 }
287
288 let one_minus_eps = 1.0 - epsilon;
290 let mut sum_pow = 0.0f64;
291 for i in 0..n {
292 sum_pow += incomes[i].powf(one_minus_eps);
293 }
294 let mean_pow = sum_pow / n as f64;
295 let power_mean = if one_minus_eps.abs() > 1e-12 {
296 mean_pow.powf(1.0 / one_minus_eps)
297 } else {
298 arithmetic_mean
299 };
300 let a = 1.0 - (power_mean / arithmetic_mean);
301 if !a.is_finite() {
302 return Err(WelfareError::NonFinite);
303 }
304 Ok(a.clamp(0.0, 1.0))
305}
306
307pub fn headcount_poverty(incomes: &[f64], poverty_line: f64) -> Result<(usize, f64), WelfareError> {
316 let n = validate_population(incomes)?;
317 if !finite_strictly_positive(poverty_line) {
318 return Err(WelfareError::InvalidInput);
319 }
320 let mut count = 0usize;
321 for &x in incomes {
322 if x < poverty_line {
323 count += 1;
324 }
325 }
326 let ratio = count as f64 / n as f64;
327 Ok((count, ratio))
328}
329
330pub fn poverty_gap_ratio(incomes: &[f64], poverty_line: f64) -> Result<f64, WelfareError> {
338 let n = validate_population(incomes)?;
339 if !finite_strictly_positive(poverty_line) {
340 return Err(WelfareError::InvalidInput);
341 }
342 let mut gap = 0.0f64;
343 for &x in incomes {
344 if x < poverty_line {
345 gap += poverty_line - x;
346 }
347 }
348 Ok(gap / (n as f64 * poverty_line))
349}
350
351pub fn utilitarian_welfare(utilities: &[f64]) -> Result<f64, WelfareError> {
362 if utilities.is_empty() {
363 return Err(WelfareError::InsufficientData);
364 }
365 if utilities.len() > MAX_POPULATION {
366 return Err(WelfareError::BufferTooSmall);
367 }
368 let mut sum = 0.0f64;
369 for &u in utilities {
370 if !finite(u) {
371 return Err(WelfareError::NonFinite);
372 }
373 sum += u;
374 }
375 Ok(sum)
376}
377
378pub fn rawlsian_welfare(utilities: &[f64]) -> Result<f64, WelfareError> {
385 if utilities.is_empty() {
386 return Err(WelfareError::InsufficientData);
387 }
388 if utilities.len() > MAX_POPULATION {
389 return Err(WelfareError::BufferTooSmall);
390 }
391 let mut min = utilities[0];
392 if !finite(min) {
393 return Err(WelfareError::NonFinite);
394 }
395 for &u in &utilities[1..] {
396 if !finite(u) {
397 return Err(WelfareError::NonFinite);
398 }
399 if u < min {
400 min = u;
401 }
402 }
403 Ok(min)
404}
405
406pub fn nash_welfare(utilities: &[f64]) -> Result<f64, WelfareError> {
417 let n = validate_positive_population(utilities)?;
418 let mut product = 1.0f64;
419 for i in 0..n {
420 product *= utilities[i];
421 }
422 if !product.is_finite() {
423 return Err(WelfareError::NonFinite);
424 }
425 Ok(product)
426}
427
428pub fn net_present_value(
443 benefits: &[f64],
444 costs: &[f64],
445 discount_rate: f64,
446 n_periods: usize,
447) -> Result<f64, WelfareError> {
448 if n_periods == 0 {
449 return Err(WelfareError::InsufficientData);
450 }
451 if n_periods > MAX_POPULATION {
452 return Err(WelfareError::BufferTooSmall);
453 }
454 if benefits.len() < n_periods || costs.len() < n_periods {
455 return Err(WelfareError::InvalidInput);
456 }
457 if !finite(discount_rate) || discount_rate <= -1.0 {
458 return Err(WelfareError::InvalidInput);
459 }
460 let one_plus_r = 1.0 + discount_rate;
461 let mut npv = 0.0f64;
462 let mut discount = 1.0f64; for t in 0..n_periods {
468 let b = benefits[t];
469 let c = costs[t];
470 if !finite(b) || !finite(c) {
471 return Err(WelfareError::NonFinite);
472 }
473 npv += (b - c) * discount;
474 discount /= one_plus_r;
475 }
476 if !npv.is_finite() {
477 return Err(WelfareError::NonFinite);
478 }
479 Ok(npv)
480}
481
482pub fn distributional_npv(
502 benefits: &[f64],
503 costs: &[f64],
504 weights: &[f64],
505 discount_rate: f64,
506 n_periods: usize,
507) -> Result<WelfareReport, WelfareError> {
508 if n_periods == 0 {
509 return Err(WelfareError::InsufficientData);
510 }
511 if n_periods > MAX_POPULATION {
512 return Err(WelfareError::BufferTooSmall);
513 }
514 if benefits.len() < n_periods || costs.len() < n_periods || weights.len() < n_periods {
515 return Err(WelfareError::InvalidInput);
516 }
517 if !finite(discount_rate) || discount_rate <= -1.0 {
518 return Err(WelfareError::InvalidInput);
519 }
520 let one_plus_r = 1.0 + discount_rate;
521 let mut weighted_npv = 0.0f64;
522 let mut unweighted_npv = 0.0f64;
523 let mut discount = 1.0f64;
524 for t in 0..n_periods {
525 let b = benefits[t];
526 let c = costs[t];
527 let w = weights[t];
528 if !finite(b) || !finite(c) || !finite(w) {
529 return Err(WelfareError::NonFinite);
530 }
531 if w < 0.0 {
532 return Err(WelfareError::InvalidInput);
533 }
534 let flow = (b - c) * discount;
535 weighted_npv += w * flow;
536 unweighted_npv += flow;
537 discount /= one_plus_r;
538 }
539 if !weighted_npv.is_finite() || !unweighted_npv.is_finite() {
540 return Err(WelfareError::NonFinite);
541 }
542 Ok(WelfareReport {
543 value: weighted_npv,
544 auxiliary: unweighted_npv,
545 assumptions: ASSUMPTION_WEIGHTED,
546 diagnostics: 0,
547 })
548}
549
550pub fn survival_floor_allocation_into(
575 needs: &[f64],
576 floors: &[f64],
577 budget: f64,
578 out: &mut [f64],
579) -> Result<WelfareReport, WelfareError> {
580 if needs.is_empty() {
581 return Err(WelfareError::InsufficientData);
582 }
583 let n = needs.len();
584 if n > MAX_POPULATION {
585 return Err(WelfareError::BufferTooSmall);
586 }
587 if floors.len() != n || out.len() < n {
588 return Err(WelfareError::InvalidInput);
589 }
590 if !finite(budget) || budget < 0.0 {
591 return Err(WelfareError::InvalidInput);
592 }
593
594 let mut total_floor = 0.0f64;
595 let mut total_surplus_need = 0.0f64;
596 for i in 0..n {
597 if !finite(needs[i]) || !finite(floors[i]) {
598 return Err(WelfareError::NonFinite);
599 }
600 if needs[i] < 0.0 || floors[i] < 0.0 {
601 return Err(WelfareError::InvalidInput);
602 }
603 if floors[i] > needs[i] {
604 return Err(WelfareError::InvalidInput);
606 }
607 total_floor += floors[i];
608 total_surplus_need += needs[i] - floors[i];
609 }
610
611 let mut assumptions: u32 = 0;
612 let mut total_allocated = 0.0f64;
613 let mut residual_distributed = 0.0f64;
614
615 if budget >= total_floor {
616 let residual = budget - total_floor;
618 for i in 0..n {
619 let base = floors[i];
620 let extra = if total_surplus_need > 0.0 {
621 residual * (needs[i] - floors[i]) / total_surplus_need
622 } else {
623 0.0
624 };
625 let alloc = base + extra;
626 out[i] = alloc;
627 total_allocated += alloc;
628 residual_distributed += extra;
629 }
630 } else {
631 assumptions |= ASSUMPTION_DEGENERATE;
634 let scale = if total_floor > 0.0 {
635 budget / total_floor
636 } else {
637 0.0
638 };
639 for i in 0..n {
640 let alloc = floors[i] * scale;
641 out[i] = alloc;
642 total_allocated += alloc;
643 }
644 assumptions |= ASSUMPTION_FLOOR_CLAMPED;
645 }
646
647 if !total_allocated.is_finite() || !residual_distributed.is_finite() {
648 return Err(WelfareError::NonFinite);
649 }
650
651 Ok(WelfareReport {
652 value: total_allocated,
653 auxiliary: residual_distributed,
654 assumptions,
655 diagnostics: 0,
656 })
657}
658
659#[cfg(test)]
664mod tests {
665 use super::*;
666
667 fn approx(a: f64, b: f64) -> bool {
668 (a - b).abs() < 1e-9
669 }
670
671 #[test]
674 fn gini_perfect_equality_is_zero() {
675 let incomes = [10.0, 10.0, 10.0];
676 let g = gini_coefficient(&incomes).unwrap();
677 assert!(approx(g, 0.0));
678 }
679
680 #[test]
681 fn gini_extreme_inequality_approaches_one_minus_one_over_n() {
682 let incomes = [0.0, 0.0, 100.0];
684 let g = gini_coefficient(&incomes).unwrap();
685 let expected = 1.0 - 1.0 / 3.0;
686 assert!(approx(g, expected));
687 }
688
689 #[test]
690 fn gini_two_person_split_is_one_half() {
691 let incomes = [0.0, 1.0];
693 let g = gini_coefficient(&incomes).unwrap();
694 assert!(approx(g, 0.5));
695 }
696
697 #[test]
698 fn gini_empty_is_insufficient_data() {
699 assert_eq!(gini_coefficient(&[]), Err(WelfareError::InsufficientData));
700 }
701
702 #[test]
703 fn gini_all_zero_is_invalid() {
704 assert_eq!(
705 gini_coefficient(&[0.0, 0.0]),
706 Err(WelfareError::InvalidInput)
707 );
708 }
709
710 #[test]
711 fn gini_nan_is_non_finite() {
712 assert_eq!(
713 gini_coefficient(&[1.0, f64::NAN]),
714 Err(WelfareError::NonFinite)
715 );
716 }
717
718 #[test]
719 fn gini_negative_is_invalid() {
720 assert_eq!(
721 gini_coefficient(&[1.0, -1.0]),
722 Err(WelfareError::InvalidInput)
723 );
724 }
725
726 #[test]
729 fn lorenz_curve_cumulative_shares() {
730 let incomes = [1.0, 2.0, 3.0];
731 let mut out = [0.0f64; 6];
732 let n = lorenz_curve_into(&incomes, &mut out).unwrap();
733 assert_eq!(n, 3);
734 assert!(approx(out[0], 1.0 / 3.0));
737 assert!(approx(out[1], 1.0 / 6.0));
738 assert!(approx(out[2], 2.0 / 3.0));
739 assert!(approx(out[3], 0.5));
740 assert!(approx(out[4], 1.0));
741 assert!(approx(out[5], 1.0));
742 }
743
744 #[test]
745 fn lorenz_buffer_too_small() {
746 let incomes = [1.0, 2.0, 3.0];
747 let mut out = [0.0f64; 5]; assert_eq!(
749 lorenz_curve_into(&incomes, &mut out),
750 Err(WelfareError::BufferTooSmall)
751 );
752 }
753
754 #[test]
755 fn lorenz_empty_is_insufficient_data() {
756 let mut out = [0.0f64; 2];
757 assert_eq!(
758 lorenz_curve_into(&[], &mut out),
759 Err(WelfareError::InsufficientData)
760 );
761 }
762
763 #[test]
766 fn atkinson_equality_is_zero() {
767 let incomes = [10.0, 10.0, 10.0];
768 let a = atkinson_inequality(&incomes, 0.5).unwrap();
769 assert!(approx(a, 0.0));
770 }
771
772 #[test]
773 fn atkinson_higher_epsilon_is_more_inequality_averse() {
774 let incomes = [1.0, 2.0, 10.0];
775 let a_low = atkinson_inequality(&incomes, 0.5).unwrap();
776 let a_high = atkinson_inequality(&incomes, 2.0).unwrap();
777 assert!(a_high > a_low, "higher epsilon must yield higher A");
778 }
779
780 #[test]
781 fn atkinson_epsilon_one_uses_log_form() {
782 let incomes = [1.0, 2.0, 4.0];
783 let a = atkinson_inequality(&incomes, 1.0).unwrap();
784 assert!(approx(a, 1.0 / 7.0));
788 }
789
790 #[test]
791 fn atkinson_zero_income_is_invalid() {
792 assert_eq!(
793 atkinson_inequality(&[0.0, 1.0], 0.5),
794 Err(WelfareError::InvalidInput)
795 );
796 }
797
798 #[test]
799 fn atkinson_nonpositive_epsilon_is_invalid() {
800 assert_eq!(
801 atkinson_inequality(&[1.0, 2.0], 0.0),
802 Err(WelfareError::InvalidInput)
803 );
804 assert_eq!(
805 atkinson_inequality(&[1.0, 2.0], -1.0),
806 Err(WelfareError::InvalidInput)
807 );
808 }
809
810 #[test]
813 fn headcount_counts_poor_and_ratio() {
814 let incomes = [5.0, 15.0, 25.0];
815 let (count, ratio) = headcount_poverty(&incomes, 10.0).unwrap();
816 assert_eq!(count, 1);
817 assert!(approx(ratio, 1.0 / 3.0));
818 }
819
820 #[test]
821 fn headcount_none_poor() {
822 let incomes = [20.0, 30.0];
823 let (count, ratio) = headcount_poverty(&incomes, 10.0).unwrap();
824 assert_eq!(count, 0);
825 assert!(approx(ratio, 0.0));
826 }
827
828 #[test]
829 fn headcount_line_at_income_excludes_boundary() {
830 let incomes = [10.0, 20.0];
832 let (count, _) = headcount_poverty(&incomes, 10.0).unwrap();
833 assert_eq!(count, 0);
834 }
835
836 #[test]
837 fn headcount_zero_line_is_invalid() {
838 assert_eq!(
839 headcount_poverty(&[1.0, 2.0], 0.0),
840 Err(WelfareError::InvalidInput)
841 );
842 }
843
844 #[test]
847 fn poverty_gap_ratio_basic() {
848 let incomes = [5.0, 15.0, 25.0];
849 let g = poverty_gap_ratio(&incomes, 10.0).unwrap();
850 assert!(approx(g, 1.0 / 6.0));
852 }
853
854 #[test]
855 fn poverty_gap_none_poor_is_zero() {
856 let incomes = [20.0, 30.0];
857 let g = poverty_gap_ratio(&incomes, 10.0).unwrap();
858 assert!(approx(g, 0.0));
859 }
860
861 #[test]
862 fn poverty_gap_all_zero_is_one() {
863 let incomes = [0.0, 0.0];
864 let g = poverty_gap_ratio(&incomes, 10.0).unwrap();
865 assert!(approx(g, 1.0));
866 }
867
868 #[test]
871 fn utilitarian_is_sum() {
872 let u = [1.0, 2.0, 3.0];
873 assert!(approx(utilitarian_welfare(&u).unwrap(), 6.0));
874 }
875
876 #[test]
877 fn utilitarian_allows_negative() {
878 let u = [-1.0, 2.0, 3.0];
879 assert!(approx(utilitarian_welfare(&u).unwrap(), 4.0));
880 }
881
882 #[test]
883 fn rawlsian_is_min() {
884 let u = [1.0, 2.0, 3.0];
885 assert!(approx(rawlsian_welfare(&u).unwrap(), 1.0));
886 }
887
888 #[test]
889 fn rawlsian_negative_min() {
890 let u = [-5.0, 2.0, 3.0];
891 assert!(approx(rawlsian_welfare(&u).unwrap(), -5.0));
892 }
893
894 #[test]
895 fn nash_is_product() {
896 let u = [1.0, 2.0, 3.0];
897 assert!(approx(nash_welfare(&u).unwrap(), 6.0));
898 }
899
900 #[test]
901 fn nash_zero_utility_is_invalid() {
902 assert_eq!(nash_welfare(&[0.0, 2.0]), Err(WelfareError::InvalidInput));
903 }
904
905 #[test]
906 fn nash_negative_utility_is_invalid() {
907 assert_eq!(nash_welfare(&[-1.0, 2.0]), Err(WelfareError::InvalidInput));
908 }
909
910 #[test]
911 fn welfare_empty_is_insufficient_data() {
912 assert_eq!(
913 utilitarian_welfare(&[]),
914 Err(WelfareError::InsufficientData)
915 );
916 assert_eq!(rawlsian_welfare(&[]), Err(WelfareError::InsufficientData));
917 assert_eq!(nash_welfare(&[]), Err(WelfareError::InsufficientData));
918 }
919
920 #[test]
921 fn welfare_nan_is_non_finite() {
922 assert_eq!(
923 utilitarian_welfare(&[1.0, f64::NAN]),
924 Err(WelfareError::NonFinite)
925 );
926 assert_eq!(
927 rawlsian_welfare(&[1.0, f64::NAN]),
928 Err(WelfareError::NonFinite)
929 );
930 }
931
932 #[test]
935 fn npv_basic_discounting() {
936 let benefits = [10.0, 10.0];
937 let costs = [5.0, 5.0];
938 let r = 0.1;
939 let npv = net_present_value(&benefits, &costs, r, 2).unwrap();
940 let expected = 5.0 + 5.0 / 1.1;
944 assert!(approx(npv, expected));
945 }
946
947 #[test]
948 fn npv_period_zero_not_discounted() {
949 let benefits = [100.0, 0.0];
950 let costs = [0.0, 0.0];
951 let npv = net_present_value(&benefits, &costs, 0.5, 2).unwrap();
952 assert!(approx(npv, 100.0));
953 }
954
955 #[test]
956 fn npv_zero_rate_is_sum() {
957 let benefits = [10.0, 10.0, 10.0];
958 let costs = [1.0, 2.0, 3.0];
959 let npv = net_present_value(&benefits, &costs, 0.0, 3).unwrap();
960 assert!(approx(npv, 24.0));
961 }
962
963 #[test]
964 fn npv_length_mismatch_is_invalid() {
965 let benefits = [10.0];
966 let costs = [5.0, 5.0];
967 assert_eq!(
968 net_present_value(&benefits, &costs, 0.1, 2),
969 Err(WelfareError::InvalidInput)
970 );
971 }
972
973 #[test]
974 fn npv_zero_periods_is_insufficient_data() {
975 assert_eq!(
976 net_present_value(&[1.0], &[1.0], 0.1, 0),
977 Err(WelfareError::InsufficientData)
978 );
979 }
980
981 #[test]
982 fn npv_rate_at_minus_one_is_invalid() {
983 assert_eq!(
984 net_present_value(&[1.0], &[1.0], -1.0, 1),
985 Err(WelfareError::InvalidInput)
986 );
987 }
988
989 #[test]
992 fn distributional_npv_applies_weights() {
993 let benefits = [10.0, 10.0];
994 let costs = [5.0, 5.0];
995 let weights = [1.0, 2.0];
996 let r = 0.1;
997 let report = distributional_npv(&benefits, &costs, &weights, r, 2).unwrap();
998 let expected_weighted = 1.0 * 5.0 + 2.0 * 5.0 / 1.1;
1000 let expected_unweighted = 5.0 + 5.0 / 1.1;
1001 assert!(approx(report.value, expected_weighted));
1002 assert!(approx(report.auxiliary, expected_unweighted));
1003 assert!(report.assumptions & ASSUMPTION_WEIGHTED != 0);
1004 }
1005
1006 #[test]
1007 fn distributional_npv_negative_weight_is_invalid() {
1008 assert_eq!(
1009 distributional_npv(&[10.0], &[5.0], &[-1.0], 0.1, 1),
1010 Err(WelfareError::InvalidInput)
1011 );
1012 }
1013
1014 #[test]
1015 fn distributional_npv_weights_length_mismatch() {
1016 assert_eq!(
1017 distributional_npv(&[10.0, 10.0], &[5.0, 5.0], &[1.0], 0.1, 2),
1018 Err(WelfareError::InvalidInput)
1019 );
1020 }
1021
1022 #[test]
1025 fn allocation_covers_floors_then_distributes_residual() {
1026 let needs = [10.0, 20.0, 30.0];
1027 let floors = [4.0, 5.0, 6.0];
1028 let budget = 25.0; let mut out = [0.0f64; 3];
1030 let report = survival_floor_allocation_into(&needs, &floors, budget, &mut out).unwrap();
1031 let expected = [
1034 4.0 + 10.0 * 6.0 / 45.0,
1035 5.0 + 10.0 * 15.0 / 45.0,
1036 6.0 + 10.0 * 24.0 / 45.0,
1037 ];
1038 for i in 0..3 {
1039 assert!(approx(out[i], expected[i]));
1040 }
1041 assert!(approx(report.value, budget));
1042 assert!(approx(report.auxiliary, 10.0));
1043 assert_eq!(report.assumptions, 0);
1044 }
1045
1046 #[test]
1047 fn allocation_under_budget_scales_floors() {
1048 let needs = [10.0, 20.0];
1049 let floors = [4.0, 6.0];
1050 let budget = 5.0; let mut out = [0.0f64; 2];
1052 let report = survival_floor_allocation_into(&needs, &floors, budget, &mut out).unwrap();
1053 assert!(approx(out[0], 2.0));
1055 assert!(approx(out[1], 3.0));
1056 assert!(approx(report.value, 5.0));
1057 assert!(report.assumptions & ASSUMPTION_DEGENERATE != 0);
1058 assert!(report.assumptions & ASSUMPTION_FLOOR_CLAMPED != 0);
1059 }
1060
1061 #[test]
1062 fn allocation_floor_exceeds_need_is_invalid() {
1063 assert_eq!(
1064 survival_floor_allocation_into(&[5.0], &[10.0], 100.0, &mut [0.0]),
1065 Err(WelfareError::InvalidInput)
1066 );
1067 }
1068
1069 #[test]
1070 fn allocation_buffer_too_small() {
1071 let needs = [10.0, 20.0];
1072 let floors = [4.0, 6.0];
1073 let mut out = [0.0f64; 1];
1074 assert_eq!(
1075 survival_floor_allocation_into(&needs, &floors, 100.0, &mut out),
1076 Err(WelfareError::InvalidInput)
1077 );
1078 }
1079
1080 #[test]
1081 fn allocation_empty_is_insufficient_data() {
1082 assert_eq!(
1083 survival_floor_allocation_into(&[], &[], 100.0, &mut []),
1084 Err(WelfareError::InsufficientData)
1085 );
1086 }
1087
1088 #[test]
1091 fn error_is_caller_error_classification() {
1092 assert!(WelfareError::InvalidInput.is_caller_error());
1093 assert!(WelfareError::InsufficientData.is_caller_error());
1094 assert!(WelfareError::BufferTooSmall.is_caller_error());
1095 assert!(!WelfareError::NonFinite.is_caller_error());
1096 }
1097
1098 #[test]
1099 fn welfare_report_clean_constructor() {
1100 let r = WelfareReport::clean(1.0, 2.0);
1101 assert_eq!(r.value, 1.0);
1102 assert_eq!(r.auxiliary, 2.0);
1103 assert_eq!(r.assumptions, 0);
1104 assert_eq!(r.diagnostics, 0);
1105 }
1106
1107 #[test]
1108 fn welfare_report_with_assumptions_constructor() {
1109 let r = WelfareReport::with_assumptions(1.0, 2.0, ASSUMPTION_WEIGHTED);
1110 assert!(r.assumptions & ASSUMPTION_WEIGHTED != 0);
1111 }
1112
1113 #[test]
1114 fn over_capacity_population_is_buffer_too_small() {
1115 let big = [1.0f64; MAX_POPULATION + 1];
1116 let slice = &big[..MAX_POPULATION + 1];
1118 assert_eq!(gini_coefficient(slice), Err(WelfareError::BufferTooSmall));
1119 }
1120}