Skip to main content

qualia_core_db/specialized_libs/computational_economics/
mechanism.rs

1//! Mechanism design: individual rationality, budget balance, VCG payments,
2//! and strategy-proofness checks.
3//!
4//! Allocation class: **HotZeroHeap**. No `Vec`/`String`/`Box` in any kernel.
5//!
6//! Assumptions:
7//! - Quasilinear utility: `u_i = v_i(allocation) - payment_i`.
8//! - Private values (each agent knows their own valuation).
9//! - Risk-neutral agents.
10
11/// Maximum agents in a bounded mechanism.
12pub const MAX_AGENTS: usize = 32;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum MechanismError {
16    InvalidInput,
17    NonFinite,
18    BufferTooSmall,
19    PropertyViolated,
20}
21
22/// Mechanism property report.
23#[derive(Debug, Clone, Copy)]
24#[repr(C)]
25pub struct MechanismReport {
26    pub individual_rationality: bool,
27    pub budget_balance: bool,
28    pub strategy_proof: bool,
29    pub total_payment: f64,
30    pub total_surplus: f64,
31}
32
33fn require_finite(x: f64) -> Result<(), MechanismError> {
34    if x.is_finite() {
35        Ok(())
36    } else {
37        Err(MechanismError::NonFinite)
38    }
39}
40
41/// Check individual rationality: every agent's payment must be <= their
42/// valuation (no agent loses by participating).
43pub fn check_individual_rationality(
44    valuations: &[f64],
45    payments: &[f64],
46) -> Result<bool, MechanismError> {
47    if valuations.is_empty() || valuations.len() != payments.len() {
48        return Err(MechanismError::InvalidInput);
49    }
50    if valuations.len() > MAX_AGENTS {
51        return Err(MechanismError::BufferTooSmall);
52    }
53    for i in 0..valuations.len() {
54        require_finite(valuations[i])?;
55        require_finite(payments[i])?;
56        if payments[i] > valuations[i] {
57            return Ok(false);
58        }
59    }
60    Ok(true)
61}
62
63/// Check budget balance: returns `(is_balanced, net_transfer)`.
64///
65/// Convention: balanced if `sum(payments) == 0` (budget balanced); no deficit
66/// if `sum(payments) >= 0`. Returns `is_balanced = (net_transfer >= 0.0)`.
67pub fn check_budget_balance(payments: &[f64]) -> Result<(bool, f64), MechanismError> {
68    if payments.is_empty() {
69        return Err(MechanismError::InvalidInput);
70    }
71    if payments.len() > MAX_AGENTS {
72        return Err(MechanismError::BufferTooSmall);
73    }
74    let mut total = 0.0;
75    for p in payments {
76        require_finite(*p)?;
77        total += p;
78    }
79    Ok((total >= -1e-10, total))
80}
81
82/// VCG (Clarke pivot) payment for single-item allocation to highest bidder.
83///
84/// The winner pays the second-highest valuation (Vickrey price). Writes
85/// payments into `out[..n]` (0 for non-winners). Returns total revenue.
86pub fn vickrey_clarke_groves_payment_into(
87    valuations: &[f64],
88    out: &mut [f64],
89) -> Result<f64, MechanismError> {
90    if valuations.is_empty() || out.len() < valuations.len() {
91        return Err(MechanismError::BufferTooSmall);
92    }
93    if valuations.len() > MAX_AGENTS {
94        return Err(MechanismError::BufferTooSmall);
95    }
96    for v in valuations {
97        require_finite(*v)?;
98        if *v < 0.0 {
99            return Err(MechanismError::InvalidInput);
100        }
101    }
102    let n = valuations.len();
103    // Find winner (highest valuation, ties by lowest index).
104    let mut winner = 0;
105    let mut highest = valuations[0];
106    for i in 1..n {
107        if valuations[i] > highest {
108            highest = valuations[i];
109            winner = i;
110        }
111    }
112    // Find second-highest.
113    let mut second = 0.0;
114    for i in 0..n {
115        if i == winner {
116            continue;
117        }
118        if valuations[i] > second {
119            second = valuations[i];
120        }
121    }
122    for i in 0..n {
123        out[i] = if i == winner { second } else { 0.0 };
124    }
125    Ok(second)
126}
127
128/// Check strategy-proofness for a 2-agent, 2-type mechanism using precomputed
129/// allocation and payment tables.
130///
131/// `valuation_matrix[agent][type]` = agent's valuation for their type.
132/// `allocation_rule[type_i][type_j]` = true if agent 0 gets the item when
133/// agent 0 reports `type_i` and agent 1 reports `type_j`.
134/// `payment_rule[type_i][type_j]` = payment by agent 0.
135///
136/// Checks that truthful reporting weakly dominates misreporting for agent 0.
137/// (Agent 1's check is symmetric and omitted for brevity; full check requires
138/// both agents.)
139pub fn check_strategy_proofness_2x2(
140    valuation_matrix: &[f64], // 2x2: [agent][type]
141    allocation_rule: &[bool], // 2x2: [type_i][type_j] → agent 0 gets item?
142    payment_rule: &[f64],     // 2x2: [type_i][type_j] → payment by agent 0
143) -> Result<bool, MechanismError> {
144    if valuation_matrix.len() < 4 || allocation_rule.len() < 4 || payment_rule.len() < 4 {
145        return Err(MechanismError::InvalidInput);
146    }
147    for v in valuation_matrix {
148        require_finite(*v)?;
149    }
150    for p in payment_rule {
151        require_finite(*p)?;
152    }
153    // For agent 0 with true type t, check:
154    // utility(truthful) >= utility(misreport) for all opponent types.
155    for true_type in 0..2 {
156        for opponent_type in 0..2 {
157            // Truthful utility.
158            let truthful_alloc = allocation_rule[true_type * 2 + opponent_type];
159            let truthful_payment = payment_rule[true_type * 2 + opponent_type];
160            let truthful_utility = if truthful_alloc {
161                valuation_matrix[0 * 2 + true_type] - truthful_payment
162            } else {
163                -truthful_payment
164            };
165            // Misreport utility (report the other type).
166            let misreport_type = 1 - true_type;
167            let misreport_alloc = allocation_rule[misreport_type * 2 + opponent_type];
168            let misreport_payment = payment_rule[misreport_type * 2 + opponent_type];
169            let misreport_utility = if misreport_alloc {
170                valuation_matrix[0 * 2 + true_type] - misreport_payment
171            } else {
172                -misreport_payment
173            };
174            if misreport_utility > truthful_utility + 1e-10 {
175                return Ok(false);
176            }
177        }
178    }
179    Ok(true)
180}
181
182/// Compute a mechanism report: IR, budget balance, total payment, and surplus.
183///
184/// `valuations` are the agents' actual valuations; `payments` are what they
185/// pay. `allocations` (bool slice) indicates who received the item.
186/// `strategy_proof` is set to `false` (not checkable generically here).
187pub fn mechanism_report(
188    valuations: &[f64],
189    payments: &[f64],
190    allocations: &[bool],
191) -> Result<MechanismReport, MechanismError> {
192    if valuations.is_empty()
193        || valuations.len() != payments.len()
194        || valuations.len() != allocations.len()
195    {
196        return Err(MechanismError::InvalidInput);
197    }
198    if valuations.len() > MAX_AGENTS {
199        return Err(MechanismError::BufferTooSmall);
200    }
201    let ir = check_individual_rationality(valuations, payments)?;
202    let (bb, total_payment) = check_budget_balance(payments)?;
203    let mut total_surplus = 0.0;
204    for i in 0..valuations.len() {
205        if allocations[i] {
206            total_surplus += valuations[i];
207        }
208        total_surplus -= payments[i];
209    }
210    Ok(MechanismReport {
211        individual_rationality: ir,
212        budget_balance: bb,
213        strategy_proof: false,
214        total_payment,
215        total_surplus,
216    })
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    fn approx(a: f64, b: f64, tol: f64) -> bool {
224        (a - b).abs() < tol
225    }
226
227    #[test]
228    fn ir_holds_when_payments_leq_valuations() {
229        let v = [10.0, 20.0, 15.0];
230        let p = [5.0, 15.0, 10.0];
231        assert!(check_individual_rationality(&v, &p).unwrap());
232    }
233
234    #[test]
235    fn ir_violated_when_payment_exceeds_valuation() {
236        let v = [10.0, 20.0, 15.0];
237        let p = [5.0, 25.0, 10.0]; // agent 1 pays 25 > 20
238        assert!(!check_individual_rationality(&v, &p).unwrap());
239    }
240
241    #[test]
242    fn budget_balance_balanced() {
243        // Payments sum to 0 (one pays, one receives).
244        let p = [10.0, -10.0];
245        let (balanced, net) = check_budget_balance(&p).unwrap();
246        assert!(balanced);
247        assert!(approx(net, 0.0, 1e-9));
248    }
249
250    #[test]
251    fn budget_balance_no_deficit() {
252        let p = [15.0, 0.0, 0.0]; // sum = 15 >= 0
253        let (balanced, net) = check_budget_balance(&p).unwrap();
254        assert!(balanced);
255        assert!(approx(net, 15.0, 1e-9));
256    }
257
258    #[test]
259    fn budget_balance_deficit() {
260        let p = [-5.0, -5.0]; // sum = -10 < 0
261        let (balanced, net) = check_budget_balance(&p).unwrap();
262        assert!(!balanced);
263        assert!(approx(net, -10.0, 1e-9));
264    }
265
266    #[test]
267    fn vcg_second_price() {
268        // Bids [10, 20, 15] → winner = 1, payment = 15 (second highest)
269        let v = [10.0, 20.0, 15.0];
270        let mut p = [0.0f64; 3];
271        let revenue = vickrey_clarke_groves_payment_into(&v, &mut p).unwrap();
272        assert!(approx(p[1], 15.0, 1e-9));
273        assert!(approx(p[0], 0.0, 1e-9));
274        assert!(approx(p[2], 0.0, 1e-9));
275        assert!(approx(revenue, 15.0, 1e-9));
276    }
277
278    #[test]
279    fn vcg_two_bidders() {
280        let v = [10.0, 20.0];
281        let mut p = [0.0f64; 2];
282        let revenue = vickrey_clarke_groves_payment_into(&v, &mut p).unwrap();
283        assert!(approx(p[1], 10.0, 1e-9));
284        assert!(approx(revenue, 10.0, 1e-9));
285    }
286
287    #[test]
288    fn strategy_proof_vickrey() {
289        // Vickrey (2nd-price) is strategy-proof.
290        // Agent 0 valuations: type 0 → 10, type 1 → 20.
291        // Allocation: highest bidder wins. Payment = second highest.
292        // Agent 1 always has valuation 15.
293        // type_i=0 (agent 0 reports 10): opponent=15 → agent 0 loses, payment=0
294        // type_i=1 (agent 0 reports 20): opponent=15 → agent 0 wins, payment=15
295        let val_matrix = [10.0, 20.0, 15.0, 15.0]; // [agent0_type0, agent0_type1, agent1_type0, agent1_type1]
296                                                   // allocation_rule[type_i][type_j] for agent 0:
297                                                   // [0][0]: report 10, opp 15 → lose → false
298                                                   // [0][1]: report 10, opp 15 → lose → false
299                                                   // [1][0]: report 20, opp 15 → win → true
300                                                   // [1][1]: report 20, opp 15 → win → true
301        let alloc = [false, false, true, true];
302        // payment_rule[type_i][type_j] for agent 0:
303        // [0][*]: lose → 0
304        // [1][*]: win → pay 15
305        let payment = [0.0, 0.0, 15.0, 15.0];
306        let sp = check_strategy_proofness_2x2(&val_matrix, &alloc, &payment).unwrap();
307        assert!(sp, "Vickrey should be strategy-proof");
308    }
309
310    #[test]
311    fn strategy_proof_first_price_not() {
312        // First-price: winner pays their bid → not strategy-proof.
313        // Construct a case where misreporting strictly helps:
314        // Agent 0 type 0 (val 10), type 1 (val 20). Opponent always has val 5.
315        let val_matrix = [10.0, 20.0, 5.0, 5.0];
316        // type 0 (val 10): report 10 → win (opp 5) → pay 10, util = 0
317        //   misreport type 1 (report 20): win → pay 20, util = -10. Worse.
318        // type 1 (val 20): report 20 → win → pay 20, util = 0
319        //   misreport type 0 (report 10): win → pay 10, util = 10. Better! → not SP.
320        let alloc = [true, true, true, true]; // always wins (bid > 5)
321        let payment = [10.0, 10.0, 20.0, 20.0]; // first-price: pays their report
322        let sp = check_strategy_proofness_2x2(&val_matrix, &alloc, &payment).unwrap();
323        assert!(!sp, "First-price should not be strategy-proof");
324    }
325
326    #[test]
327    fn mechanism_report_vickrey() {
328        let v = [10.0, 20.0, 15.0];
329        let mut p = [0.0f64; 3];
330        vickrey_clarke_groves_payment_into(&v, &mut p).unwrap();
331        let alloc = [false, true, false]; // agent 1 wins
332        let report = mechanism_report(&v, &p, &alloc).unwrap();
333        assert!(report.individual_rationality); // 15 <= 20
334        assert!(report.budget_balance); // revenue = 15 >= 0
335        assert!(approx(report.total_payment, 15.0, 1e-9));
336        assert!(approx(report.total_surplus, 20.0 - 15.0, 1e-9)); // winner val - payment
337    }
338
339    #[test]
340    fn empty_rejected() {
341        assert_eq!(
342            check_individual_rationality(&[], &[]).unwrap_err(),
343            MechanismError::InvalidInput
344        );
345    }
346}