1use std::{any::Any, sync::Arc};
4
5use sim_kernel::{
6 Claim, Cx, Datum, Error, Expr, Factory, NumberLiteral, Object, ObjectCompat, Ref, Result,
7 Symbol, Value,
8};
9use sim_lib_numbers_core::domains;
10
11use crate::{BinaryOutcomeCounts, StatsResult, disparate_impact};
12
13const FOUR_FIFTHS_THRESHOLD: f64 = 0.8;
14
15#[derive(Clone, Debug, PartialEq)]
17pub struct FairnessEvidence {
18 pub reference_selected: u64,
20 pub reference_total: u64,
22 pub comparison_selected: u64,
24 pub comparison_total: u64,
26 pub reference_rate: f64,
28 pub comparison_rate: f64,
30 pub ratio: f64,
32 pub threshold: f64,
34 pub passes_four_fifths: bool,
36}
37
38impl FairnessEvidence {
39 pub fn new(
41 reference_selected: u64,
42 reference_total: u64,
43 comparison_selected: u64,
44 comparison_total: u64,
45 ) -> StatsResult<Self> {
46 let reference = BinaryOutcomeCounts::new(reference_selected, reference_total)?;
47 let comparison = BinaryOutcomeCounts::new(comparison_selected, comparison_total)?;
48 let impact = disparate_impact(reference, comparison)?;
49 Ok(Self {
50 reference_selected,
51 reference_total,
52 comparison_selected,
53 comparison_total,
54 reference_rate: impact.reference_rate,
55 comparison_rate: impact.comparison_rate,
56 ratio: impact.ratio,
57 threshold: FOUR_FIFTHS_THRESHOLD,
58 passes_four_fifths: impact.passes_four_fifths,
59 })
60 }
61
62 pub fn table_value(&self, factory: &dyn Factory) -> Result<Value> {
64 factory.table(vec![
65 (
66 Symbol::new("reference-selected"),
67 u64_value(factory, self.reference_selected)?,
68 ),
69 (
70 Symbol::new("reference-total"),
71 u64_value(factory, self.reference_total)?,
72 ),
73 (
74 Symbol::new("comparison-selected"),
75 u64_value(factory, self.comparison_selected)?,
76 ),
77 (
78 Symbol::new("comparison-total"),
79 u64_value(factory, self.comparison_total)?,
80 ),
81 (
82 Symbol::new("reference-rate"),
83 f64_value(factory, self.reference_rate)?,
84 ),
85 (
86 Symbol::new("comparison-rate"),
87 f64_value(factory, self.comparison_rate)?,
88 ),
89 (Symbol::new("ratio"), f64_value(factory, self.ratio)?),
90 (Symbol::new("value"), f64_value(factory, self.ratio)?),
91 (
92 Symbol::new("threshold"),
93 f64_value(factory, self.threshold)?,
94 ),
95 (
96 Symbol::new("passes-four-fifths"),
97 factory.bool(self.passes_four_fifths)?,
98 ),
99 ])
100 }
101
102 fn datum(&self) -> Datum {
103 Datum::Node {
104 tag: Symbol::qualified("stats", "disparate-impact-evidence"),
105 fields: vec![
106 (
107 Symbol::new("reference-selected"),
108 u64_datum(self.reference_selected),
109 ),
110 (
111 Symbol::new("reference-total"),
112 u64_datum(self.reference_total),
113 ),
114 (
115 Symbol::new("comparison-selected"),
116 u64_datum(self.comparison_selected),
117 ),
118 (
119 Symbol::new("comparison-total"),
120 u64_datum(self.comparison_total),
121 ),
122 (
123 Symbol::new("reference-rate"),
124 f64_datum(self.reference_rate),
125 ),
126 (
127 Symbol::new("comparison-rate"),
128 f64_datum(self.comparison_rate),
129 ),
130 (Symbol::new("ratio"), f64_datum(self.ratio)),
131 (Symbol::new("value"), f64_datum(self.ratio)),
132 (Symbol::new("threshold"), f64_datum(self.threshold)),
133 (
134 Symbol::new("passes-four-fifths"),
135 Datum::Bool(self.passes_four_fifths),
136 ),
137 ],
138 }
139 }
140}
141
142#[derive(Clone, Debug, PartialEq)]
144pub struct StatsClaimEvidence {
145 metric: Symbol,
146 inputs: Vec<f64>,
147 value: f64,
148}
149
150impl StatsClaimEvidence {
151 pub fn new(metric: Symbol, inputs: Vec<f64>, value: f64) -> Self {
153 Self {
154 metric,
155 inputs,
156 value,
157 }
158 }
159
160 pub fn metric(&self) -> &Symbol {
162 &self.metric
163 }
164
165 pub fn inputs(&self) -> &[f64] {
167 &self.inputs
168 }
169
170 pub fn value(&self) -> f64 {
172 self.value
173 }
174
175 pub fn table_value(&self, factory: &dyn Factory) -> Result<Value> {
177 factory.table(vec![
178 (Symbol::new("metric"), factory.symbol(self.metric.clone())?),
179 (
180 Symbol::new("inputs"),
181 f64_list_value(factory, &self.inputs)?,
182 ),
183 (
184 Symbol::new("count"),
185 u64_value(factory, self.inputs.len() as u64)?,
186 ),
187 (Symbol::new("value"), f64_value(factory, self.value)?),
188 ])
189 }
190
191 fn datum(&self) -> Datum {
192 Datum::Node {
193 tag: Symbol::qualified("stats", "result-evidence"),
194 fields: vec![
195 (Symbol::new("metric"), Datum::Symbol(self.metric.clone())),
196 (
197 Symbol::new("inputs"),
198 Datum::Vector(self.inputs.iter().copied().map(f64_datum).collect()),
199 ),
200 (Symbol::new("count"), u64_datum(self.inputs.len() as u64)),
201 (Symbol::new("value"), f64_datum(self.value)),
202 ],
203 }
204 }
205}
206
207pub fn stats_result_claim(cx: &mut Cx, evidence: &StatsClaimEvidence) -> Result<Claim> {
209 Claim::content_object(
210 cx.datum_store_mut(),
211 Ref::Symbol(evidence.metric().clone()),
212 Symbol::new("stats-result"),
213 evidence.datum(),
214 )
215}
216
217#[derive(Clone, Debug, PartialEq)]
219pub struct StatsClaimValue {
220 claim: Claim,
221 evidence: StatsClaimEvidence,
222}
223
224impl StatsClaimValue {
225 pub fn new(claim: Claim, evidence: StatsClaimEvidence) -> Self {
227 Self { claim, evidence }
228 }
229
230 pub fn claim(&self) -> &Claim {
232 &self.claim
233 }
234
235 pub fn evidence(&self) -> &StatsClaimEvidence {
237 &self.evidence
238 }
239
240 pub fn table_value(&self, cx: &mut Cx) -> Result<Value> {
242 let object = match &self.claim.object {
243 Ref::Content(id) => cx.factory().string(format!("{id:?}"))?,
244 other => cx.factory().string(format!("{other:?}"))?,
245 };
246 cx.factory().table(vec![
247 (
248 Symbol::new("kind"),
249 cx.factory().string("claim".to_owned())?,
250 ),
251 (
252 Symbol::new("subject"),
253 cx.factory().symbol(self.evidence.metric().clone())?,
254 ),
255 (
256 Symbol::new("predicate"),
257 cx.factory().symbol(self.claim.predicate.clone())?,
258 ),
259 (Symbol::new("object"), object),
260 (
261 Symbol::new("evidence"),
262 self.evidence.table_value(cx.factory())?,
263 ),
264 ])
265 }
266}
267
268impl Object for StatsClaimValue {
269 fn snapshot(&self, _cx: &mut Cx) -> Result<Option<Datum>> {
270 Ok(Some(self.claim.canonical_datum()))
271 }
272
273 fn display(&self, _cx: &mut Cx) -> Result<String> {
274 Ok(format!(
275 "#<claim {} stats-result>",
276 self.evidence.metric().as_qualified_str()
277 ))
278 }
279
280 fn as_any(&self) -> &dyn Any {
281 self
282 }
283}
284
285impl ObjectCompat for StatsClaimValue {
286 fn as_expr(&self, _cx: &mut Cx) -> Result<Expr> {
287 Ok(Expr::from(self.claim.canonical_datum()))
288 }
289
290 fn as_table(&self, cx: &mut Cx) -> Result<Value> {
291 self.table_value(cx)
292 }
293}
294
295pub fn stats_result_claim_value(cx: &mut Cx, evidence: StatsClaimEvidence) -> Result<Value> {
297 let claim = stats_result_claim(cx, &evidence)?;
298 cx.factory()
299 .opaque(Arc::new(StatsClaimValue::new(claim, evidence)))
300}
301
302pub fn fairness_claim(cx: &mut Cx, evidence: &FairnessEvidence) -> Result<Claim> {
304 Claim::content_object(
305 cx.datum_store_mut(),
306 Ref::Symbol(Symbol::qualified("stats", "disparate-impact")),
307 Symbol::new("fairness-result"),
308 evidence.datum(),
309 )
310}
311
312#[derive(Clone, Debug, PartialEq)]
314pub struct FairnessClaimValue {
315 claim: Claim,
316 evidence: FairnessEvidence,
317}
318
319impl FairnessClaimValue {
320 pub fn new(claim: Claim, evidence: FairnessEvidence) -> Self {
322 Self { claim, evidence }
323 }
324
325 pub fn claim(&self) -> &Claim {
327 &self.claim
328 }
329
330 pub fn evidence(&self) -> &FairnessEvidence {
332 &self.evidence
333 }
334
335 pub fn table_value(&self, cx: &mut Cx) -> Result<Value> {
337 let object = match &self.claim.object {
338 Ref::Content(id) => cx.factory().string(format!("{id:?}"))?,
339 other => cx.factory().string(format!("{other:?}"))?,
340 };
341 cx.factory().table(vec![
342 (
343 Symbol::new("kind"),
344 cx.factory().string("claim".to_owned())?,
345 ),
346 (
347 Symbol::new("subject"),
348 cx.factory()
349 .symbol(Symbol::qualified("stats", "disparate-impact"))?,
350 ),
351 (
352 Symbol::new("predicate"),
353 cx.factory().symbol(self.claim.predicate.clone())?,
354 ),
355 (Symbol::new("object"), object),
356 (
357 Symbol::new("evidence"),
358 self.evidence.table_value(cx.factory())?,
359 ),
360 ])
361 }
362}
363
364impl Object for FairnessClaimValue {
365 fn snapshot(&self, _cx: &mut Cx) -> Result<Option<Datum>> {
366 Ok(Some(self.claim.canonical_datum()))
367 }
368
369 fn display(&self, _cx: &mut Cx) -> Result<String> {
370 Ok("#<claim stats/disparate-impact fairness-result>".to_owned())
371 }
372
373 fn as_any(&self) -> &dyn Any {
374 self
375 }
376}
377
378impl ObjectCompat for FairnessClaimValue {
379 fn as_expr(&self, _cx: &mut Cx) -> Result<Expr> {
380 Ok(Expr::from(self.claim.canonical_datum()))
381 }
382
383 fn as_table(&self, cx: &mut Cx) -> Result<Value> {
384 self.table_value(cx)
385 }
386}
387
388pub fn fairness_claim_value(cx: &mut Cx, evidence: FairnessEvidence) -> Result<Value> {
390 let claim = fairness_claim(cx, &evidence)?;
391 cx.factory()
392 .opaque(Arc::new(FairnessClaimValue::new(claim, evidence)))
393}
394
395fn u64_value(factory: &dyn Factory, value: u64) -> Result<Value> {
396 factory.number_literal(domains::u64(), value.to_string())
397}
398
399fn f64_value(factory: &dyn Factory, value: f64) -> Result<Value> {
400 factory.number_literal(domains::f64(), value.to_string())
401}
402
403fn f64_list_value(factory: &dyn Factory, values: &[f64]) -> Result<Value> {
404 let values = values
405 .iter()
406 .copied()
407 .map(|value| f64_value(factory, value))
408 .collect::<Result<Vec<_>>>()?;
409 factory.list(values)
410}
411
412fn u64_datum(value: u64) -> Datum {
413 Datum::Number(NumberLiteral {
414 domain: domains::u64(),
415 canonical: value.to_string(),
416 })
417}
418
419fn f64_datum(value: f64) -> Datum {
420 Datum::Number(NumberLiteral {
421 domain: domains::f64(),
422 canonical: value.to_string(),
423 })
424}
425
426pub(crate) fn stats_error_to_kernel(error: crate::StatsError) -> Error {
427 Error::DomainError {
428 domain: Symbol::qualified("numbers", "stats"),
429 category: Symbol::new("invalid-input"),
430 message: error.to_string(),
431 }
432}