datasynth_eval/coherence/
esg.rs1use crate::error::EvalResult;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct EsgThresholds {
12 pub min_metric_accuracy: f64,
14 pub min_safety_rate_accuracy: f64,
16 pub metric_tolerance: f64,
18}
19
20impl Default for EsgThresholds {
21 fn default() -> Self {
22 Self {
23 min_metric_accuracy: 0.99,
24 min_safety_rate_accuracy: 0.999,
25 metric_tolerance: 0.01,
26 }
27 }
28}
29
30#[derive(Debug, Clone)]
32pub struct WaterUsageData {
33 pub record_id: String,
35 pub withdrawal_m3: f64,
37 pub discharge_m3: f64,
39 pub consumption_m3: f64,
41}
42
43#[derive(Debug, Clone)]
45pub struct SafetyMetricData {
46 pub metric_id: String,
48 pub total_hours_worked: f64,
50 pub recordable_incidents: u32,
52 pub trir: f64,
54 pub lost_time_incidents: u32,
56 pub ltir: f64,
58}
59
60#[derive(Debug, Clone)]
62pub struct GovernanceData {
63 pub metric_id: String,
65 pub board_size: u32,
67 pub independent_directors: u32,
69 pub independence_ratio: f64,
71}
72
73#[derive(Debug, Clone)]
75pub struct SupplierEsgData {
76 pub assessment_id: String,
78 pub environmental_score: f64,
80 pub social_score: f64,
82 pub governance_score: f64,
84 pub overall_score: f64,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct EsgEvaluation {
91 pub water_accuracy: f64,
93 pub trir_accuracy: f64,
95 pub ltir_accuracy: f64,
97 pub governance_accuracy: f64,
99 pub supplier_scoring_accuracy: f64,
101 pub total_water_records: usize,
103 pub total_safety_records: usize,
105 pub total_governance_records: usize,
107 pub total_supplier_assessments: usize,
109 pub passes: bool,
111 pub issues: Vec<String>,
113}
114
115pub struct EsgEvaluator {
117 thresholds: EsgThresholds,
118}
119
120impl EsgEvaluator {
121 pub fn new() -> Self {
123 Self {
124 thresholds: EsgThresholds::default(),
125 }
126 }
127
128 pub fn with_thresholds(thresholds: EsgThresholds) -> Self {
130 Self { thresholds }
131 }
132
133 pub fn evaluate(
135 &self,
136 water: &[WaterUsageData],
137 safety: &[SafetyMetricData],
138 governance: &[GovernanceData],
139 suppliers: &[SupplierEsgData],
140 ) -> EvalResult<EsgEvaluation> {
141 let mut issues = Vec::new();
142 let tolerance = self.thresholds.metric_tolerance;
143
144 let water_ok = water
146 .iter()
147 .filter(|w| {
148 let expected = w.withdrawal_m3 - w.discharge_m3;
149 (w.consumption_m3 - expected).abs() <= tolerance * w.withdrawal_m3.abs().max(1.0)
150 })
151 .count();
152 let water_accuracy = if water.is_empty() {
153 1.0
154 } else {
155 water_ok as f64 / water.len() as f64
156 };
157
158 let trir_ok = safety
160 .iter()
161 .filter(|s| {
162 if s.total_hours_worked <= 0.0 {
163 return true;
164 }
165 let expected = s.recordable_incidents as f64 * 200_000.0 / s.total_hours_worked;
166 (s.trir - expected).abs() <= tolerance * expected.abs().max(0.001)
167 })
168 .count();
169 let trir_accuracy = if safety.is_empty() {
170 1.0
171 } else {
172 trir_ok as f64 / safety.len() as f64
173 };
174
175 let ltir_ok = safety
177 .iter()
178 .filter(|s| {
179 if s.total_hours_worked <= 0.0 {
180 return true;
181 }
182 let expected = s.lost_time_incidents as f64 * 200_000.0 / s.total_hours_worked;
183 (s.ltir - expected).abs() <= tolerance * expected.abs().max(0.001)
184 })
185 .count();
186 let ltir_accuracy = if safety.is_empty() {
187 1.0
188 } else {
189 ltir_ok as f64 / safety.len() as f64
190 };
191
192 let gov_ok = governance
194 .iter()
195 .filter(|g| {
196 if g.board_size == 0 {
197 return true;
198 }
199 let expected = g.independent_directors as f64 / g.board_size as f64;
200 (g.independence_ratio - expected).abs() <= tolerance
201 })
202 .count();
203 let governance_accuracy = if governance.is_empty() {
204 1.0
205 } else {
206 gov_ok as f64 / governance.len() as f64
207 };
208
209 let supplier_ok = suppliers
211 .iter()
212 .filter(|s| {
213 let expected = (s.environmental_score + s.social_score + s.governance_score) / 3.0;
214 (s.overall_score - expected).abs() <= tolerance * expected.abs().max(1.0)
215 })
216 .count();
217 let supplier_scoring_accuracy = if suppliers.is_empty() {
218 1.0
219 } else {
220 supplier_ok as f64 / suppliers.len() as f64
221 };
222
223 if water_accuracy < self.thresholds.min_metric_accuracy {
225 issues.push(format!(
226 "Water consumption accuracy {:.4} < {:.4}",
227 water_accuracy, self.thresholds.min_metric_accuracy
228 ));
229 }
230 if trir_accuracy < self.thresholds.min_safety_rate_accuracy {
231 issues.push(format!(
232 "TRIR accuracy {:.4} < {:.4}",
233 trir_accuracy, self.thresholds.min_safety_rate_accuracy
234 ));
235 }
236 if ltir_accuracy < self.thresholds.min_safety_rate_accuracy {
237 issues.push(format!(
238 "LTIR accuracy {:.4} < {:.4}",
239 ltir_accuracy, self.thresholds.min_safety_rate_accuracy
240 ));
241 }
242 if governance_accuracy < self.thresholds.min_metric_accuracy {
243 issues.push(format!(
244 "Governance ratio accuracy {:.4} < {:.4}",
245 governance_accuracy, self.thresholds.min_metric_accuracy
246 ));
247 }
248 if supplier_scoring_accuracy < self.thresholds.min_metric_accuracy {
249 issues.push(format!(
250 "Supplier ESG scoring accuracy {:.4} < {:.4}",
251 supplier_scoring_accuracy, self.thresholds.min_metric_accuracy
252 ));
253 }
254
255 let passes = issues.is_empty();
256
257 Ok(EsgEvaluation {
258 water_accuracy,
259 trir_accuracy,
260 ltir_accuracy,
261 governance_accuracy,
262 supplier_scoring_accuracy,
263 total_water_records: water.len(),
264 total_safety_records: safety.len(),
265 total_governance_records: governance.len(),
266 total_supplier_assessments: suppliers.len(),
267 passes,
268 issues,
269 })
270 }
271}
272
273impl Default for EsgEvaluator {
274 fn default() -> Self {
275 Self::new()
276 }
277}
278
279#[cfg(test)]
280#[allow(clippy::unwrap_used)]
281mod tests {
282 use super::*;
283
284 #[test]
285 fn test_valid_esg_data() {
286 let evaluator = EsgEvaluator::new();
287 let water = vec![WaterUsageData {
288 record_id: "W001".to_string(),
289 withdrawal_m3: 1000.0,
290 discharge_m3: 700.0,
291 consumption_m3: 300.0,
292 }];
293 let safety = vec![SafetyMetricData {
294 metric_id: "S001".to_string(),
295 total_hours_worked: 1_000_000.0,
296 recordable_incidents: 5,
297 trir: 1.0, lost_time_incidents: 2,
299 ltir: 0.4, }];
301 let governance = vec![GovernanceData {
302 metric_id: "G001".to_string(),
303 board_size: 10,
304 independent_directors: 7,
305 independence_ratio: 0.7,
306 }];
307 let suppliers = vec![SupplierEsgData {
308 assessment_id: "ESG001".to_string(),
309 environmental_score: 80.0,
310 social_score: 70.0,
311 governance_score: 90.0,
312 overall_score: 80.0,
313 }];
314
315 let result = evaluator
316 .evaluate(&water, &safety, &governance, &suppliers)
317 .unwrap();
318 assert!(result.passes);
319 assert_eq!(result.total_water_records, 1);
320 assert_eq!(result.total_safety_records, 1);
321 }
322
323 #[test]
324 fn test_wrong_water_consumption() {
325 let evaluator = EsgEvaluator::new();
326 let water = vec![WaterUsageData {
327 record_id: "W001".to_string(),
328 withdrawal_m3: 1000.0,
329 discharge_m3: 700.0,
330 consumption_m3: 500.0, }];
332
333 let result = evaluator.evaluate(&water, &[], &[], &[]).unwrap();
334 assert!(!result.passes);
335 assert!(result.issues[0].contains("Water consumption"));
336 }
337
338 #[test]
339 fn test_wrong_trir() {
340 let evaluator = EsgEvaluator::new();
341 let safety = vec![SafetyMetricData {
342 metric_id: "S001".to_string(),
343 total_hours_worked: 1_000_000.0,
344 recordable_incidents: 5,
345 trir: 5.0, lost_time_incidents: 2,
347 ltir: 0.4,
348 }];
349
350 let result = evaluator.evaluate(&[], &safety, &[], &[]).unwrap();
351 assert!(!result.passes);
352 assert!(result.issues.iter().any(|i| i.contains("TRIR")));
353 }
354
355 #[test]
356 fn test_wrong_supplier_scoring() {
357 let evaluator = EsgEvaluator::new();
358 let suppliers = vec![SupplierEsgData {
359 assessment_id: "ESG001".to_string(),
360 environmental_score: 80.0,
361 social_score: 70.0,
362 governance_score: 90.0,
363 overall_score: 90.0, }];
365
366 let result = evaluator.evaluate(&[], &[], &[], &suppliers).unwrap();
367 assert!(!result.passes);
368 assert!(result.issues[0].contains("Supplier ESG"));
369 }
370
371 #[test]
372 fn test_wrong_ltir() {
373 let evaluator = EsgEvaluator::new();
374 let safety = vec![SafetyMetricData {
375 metric_id: "S001".to_string(),
376 total_hours_worked: 1_000_000.0,
377 recordable_incidents: 5,
378 trir: 1.0, lost_time_incidents: 2,
380 ltir: 2.0, }];
382
383 let result = evaluator.evaluate(&[], &safety, &[], &[]).unwrap();
384 assert!(!result.passes);
385 assert!(result.issues.iter().any(|i| i.contains("LTIR")));
386 }
387
388 #[test]
389 fn test_wrong_governance_ratio() {
390 let evaluator = EsgEvaluator::new();
391 let governance = vec![GovernanceData {
392 metric_id: "G001".to_string(),
393 board_size: 10,
394 independent_directors: 7,
395 independence_ratio: 0.5, }];
397
398 let result = evaluator.evaluate(&[], &[], &governance, &[]).unwrap();
399 assert!(!result.passes);
400 assert!(result.issues.iter().any(|i| i.contains("Governance ratio")));
401 }
402
403 #[test]
404 fn test_empty_data() {
405 let evaluator = EsgEvaluator::new();
406 let result = evaluator.evaluate(&[], &[], &[], &[]).unwrap();
407 assert!(result.passes);
408 }
409}