datasynth_eval/privacy/
mod.rs1pub mod linkage;
10pub mod membership_inference;
11pub mod metrics;
12
13pub use linkage::{LinkageAttack, LinkageConfig, LinkageResults};
14pub use membership_inference::{MembershipInferenceAttack, MiaConfig, MiaResults};
15pub use metrics::{NistAlignmentReport, NistCriterion, SynQPMatrix, SynQPQuadrant};
16
17use serde::{Deserialize, Serialize};
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct PrivacyEvaluation {
22 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub membership_inference: Option<MiaResults>,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
27 pub linkage: Option<LinkageResults>,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
30 pub nist_alignment: Option<NistAlignmentReport>,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub synqp: Option<SynQPMatrix>,
34 pub passes: bool,
36 pub failures: Vec<String>,
38}
39
40impl Default for PrivacyEvaluation {
41 fn default() -> Self {
42 Self {
43 membership_inference: None,
44 linkage: None,
45 nist_alignment: None,
46 synqp: None,
47 passes: true,
48 failures: Vec::new(),
49 }
50 }
51}
52
53impl PrivacyEvaluation {
54 pub fn new() -> Self {
56 Self::default()
57 }
58
59 pub fn update_status(&mut self) {
61 self.failures.clear();
62
63 if let Some(ref mia) = self.membership_inference {
64 if !mia.passes {
65 self.failures.push(format!(
66 "MIA: AUC-ROC {:.4} exceeds threshold {:.4}",
67 mia.auc_roc, mia.auc_threshold
68 ));
69 }
70 }
71
72 if let Some(ref linkage) = self.linkage {
73 if !linkage.passes {
74 self.failures.push(format!(
75 "Linkage: re-identification rate {:.2}%, k-anonymity {}",
76 linkage.re_identification_rate * 100.0,
77 linkage.k_anonymity_achieved,
78 ));
79 }
80 }
81
82 if let Some(ref nist) = self.nist_alignment {
83 if !nist.passes {
84 self.failures.push(format!(
85 "NIST alignment: score {:.0}% (requires >= 71%)",
86 nist.alignment_score * 100.0
87 ));
88 }
89 }
90
91 self.passes = self.failures.is_empty();
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn test_privacy_evaluation_default() {
101 let eval = PrivacyEvaluation::default();
102 assert!(eval.passes);
103 assert!(eval.failures.is_empty());
104 assert!(eval.membership_inference.is_none());
105 assert!(eval.linkage.is_none());
106 }
107
108 #[test]
109 fn test_privacy_evaluation_with_failures() {
110 let mut eval = PrivacyEvaluation::new();
111
112 eval.membership_inference = Some(MiaResults {
113 auc_roc: 0.85,
114 accuracy: 0.80,
115 precision: 0.78,
116 recall: 0.82,
117 passes: false,
118 n_members: 100,
119 n_non_members: 100,
120 auc_threshold: 0.6,
121 });
122
123 eval.update_status();
124 assert!(!eval.passes);
125 assert_eq!(eval.failures.len(), 1);
126 assert!(eval.failures[0].contains("MIA"));
127 }
128
129 #[test]
130 fn test_privacy_evaluation_all_pass() {
131 let mut eval = PrivacyEvaluation::new();
132
133 eval.membership_inference = Some(MiaResults {
134 auc_roc: 0.52,
135 accuracy: 0.50,
136 precision: 0.50,
137 recall: 0.50,
138 passes: true,
139 n_members: 100,
140 n_non_members: 100,
141 auc_threshold: 0.6,
142 });
143
144 eval.linkage = Some(LinkageResults {
145 re_identification_rate: 0.01,
146 k_anonymity_achieved: 10,
147 unique_qi_combos_original: 50,
148 unique_qi_combos_synthetic: 48,
149 overlapping_combos: 30,
150 uniquely_linked: 1,
151 total_synthetic: 100,
152 passes: true,
153 });
154
155 eval.update_status();
156 assert!(eval.passes);
157 assert!(eval.failures.is_empty());
158 }
159
160 #[test]
161 fn test_privacy_evaluation_serde() {
162 let eval = PrivacyEvaluation::default();
163 let json = serde_json::to_string(&eval).unwrap();
164 let parsed: PrivacyEvaluation = serde_json::from_str(&json).unwrap();
165 assert!(parsed.passes);
166 }
167}