Skip to main content

automation_structures/compositions/
sampler.rs

1//! Sampler assembled from the ActuationPass and Budget primitives.
2//!
3//! ActuationPass owns the live support and selected-effect projection. Budget owns the sample
4//! ceiling. Sampler adds only the atomic coupling between an actuation and one Budget allocation,
5//! plus caller-supplied proposal admission rules.
6
7use crate::primitives::actuation_pass::ActuationPass;
8use crate::primitives::budget::Budget;
9use vstd::prelude::*;
10
11verus! {
12
13/// Integer indicator for an occupied ActuationPass slot.
14pub open spec fn present<T>(value: Option<T>) -> int {
15    if value is Some { 1 } else { 0 }
16}
17
18/// Number of present effects in the first `n` ActuationPass seats.
19pub open spec fn selected_count<T>(effects: Seq<Option<T>>, n: int) -> int
20    decreases n,
21{
22    if n <= 0 || n > effects.len() {
23        0
24    } else {
25        present(effects[n - 1]) + selected_count(effects, n - 1)
26    }
27}
28
29proof fn selected_count_none<T>(effects: Seq<Option<T>>, n: int)
30    requires
31        0 <= n <= effects.len(),
32        forall|i: int| 0 <= i < effects.len() ==> #[trigger] effects[i] is None,
33    ensures selected_count(effects, n) == 0,
34    decreases n,
35{
36    if n > 0 {
37        selected_count_none(effects, n - 1);
38    }
39}
40
41proof fn selected_count_update_unaffected<T>(
42    effects: Seq<Option<T>>,
43    index: int,
44    replacement: Option<T>,
45    n: int,
46)
47    requires 0 <= n <= index < effects.len(),
48    ensures selected_count(effects.update(index, replacement), n)
49        == selected_count(effects, n),
50    decreases n,
51{
52    if n > 0 {
53        selected_count_update_unaffected(effects, index, replacement, n - 1);
54        assert(effects.update(index, replacement)[n - 1] == effects[n - 1]);
55    }
56}
57
58proof fn selected_count_update<T>(
59    effects: Seq<Option<T>>,
60    index: int,
61    replacement: Option<T>,
62    n: int,
63)
64    requires 0 <= index < n <= effects.len(),
65    ensures selected_count(effects.update(index, replacement), n)
66        == selected_count(effects, n) - present(effects[index]) + present(replacement),
67    decreases n,
68{
69    if n == index + 1 {
70        selected_count_update_unaffected(effects, index, replacement, index);
71        assert(effects.update(index, replacement)[index] == replacement);
72    } else {
73        selected_count_update(effects, index, replacement, n - 1);
74        assert(effects.update(index, replacement)[n - 1] == effects[n - 1]);
75    }
76}
77
78/// A bounded without-replacement sampler composed from ActuationPass and Budget.
79pub struct Sampler {
80    /// Owner of support, selection, and applied effects.
81    pub actuation: ActuationPass,
82    /// Owner of the sample-size ceiling.
83    pub budget: Budget,
84}
85
86impl Sampler {
87    /// Whether one item is selected in the ActuationPass effect projection.
88    pub open spec fn contains(&self, item: usize) -> bool {
89        item < self.actuation.effects.len() && self.actuation.effects@[item as int] is Some
90    }
91
92    /// Every live ActuationPass allocation has positive support weight.
93    pub open spec fn support_domain(&self) -> bool {
94        forall|i: int| 0 <= i < self.actuation.allocation.len()
95            && #[trigger] self.actuation.allocation@[i] is Some
96            ==> self.actuation.allocation@[i]->Some_0 > 0
97    }
98
99    /// The two primitive states and their coupling are well formed.
100    pub open spec fn type_invariant(&self) -> bool {
101        &&& self.actuation.invariant()
102        &&& !self.actuation.complete
103        &&& self.support_domain()
104        &&& self.budget.safety_invariant()
105        &&& self.budget.reserved == 0
106        &&& self.budget.pending_eviction == 0
107        &&& self.budget.allocated as int
108            == selected_count(self.actuation.effects@, self.actuation.effects.len() as int)
109    }
110
111    /// Selected cardinality is owned by the Budget.
112    pub open spec fn bounded_sample(&self) -> bool {
113        self.budget.allocated <= self.budget.capacity
114    }
115
116    /// Every selected effect is tied to a positive live ActuationPass allocation.
117    pub open spec fn support_consistency(&self) -> bool {
118        forall|i: int| 0 <= i < self.actuation.effects.len()
119            && #[trigger] self.actuation.effects@[i] is Some
120            ==> self.actuation.allocation@[i] is Some
121                && self.actuation.allocation@[i]->Some_0 > 0
122    }
123
124    /// Full composition invariant.
125    pub open spec fn inv(&self) -> bool {
126        self.type_invariant() && self.bounded_sample() && self.support_consistency()
127    }
128
129    /// Build the ActuationPass support projection and an empty sample Budget.
130    pub fn new(distribution: Vec<u64>, sample_size: usize) -> (sampler: Self)
131        ensures
132            sampler.actuation.num_seats == distribution@.len(),
133            sampler.budget.capacity == sample_size as u64,
134            sampler.budget.allocated == 0,
135            sampler.inv(),
136            forall|i: int| 0 <= i < distribution@.len() ==> {
137                let allocation = #[trigger] sampler.actuation.allocation@[i];
138                if distribution@[i] == 0 {
139                    allocation is None
140                } else {
141                    allocation == Some(distribution@[i])
142                }
143            },
144    {
145        let length = distribution.len();
146        let mut allocation: Vec<Option<u64>> = Vec::new();
147        let mut index: usize = 0;
148        while index < length
149            invariant
150                index <= length,
151                length == distribution@.len(),
152                allocation.len() == index,
153                forall|i: int| 0 <= i < index ==> {
154                    let value = #[trigger] allocation@[i];
155                    if distribution@[i] == 0 {
156                        value is None
157                    } else {
158                        value == Some(distribution@[i])
159                    }
160                },
161            decreases length - index,
162        {
163            let weight = distribution[index];
164            if weight == 0 {
165                allocation.push(None);
166            } else {
167                allocation.push(Some(weight));
168            }
169            index += 1;
170        }
171        let actuation = ActuationPass::new(allocation, length);
172        let budget = Budget::new(sample_size as u64);
173        proof { selected_count_none(actuation.effects@, actuation.effects.len() as int); }
174        Self { actuation, budget }
175    }
176
177    /// Read one support weight, mapping ActuationPass's absent allocation to zero.
178    pub fn weight(&self, item: usize) -> (weight: u64)
179        requires self.inv(), item < self.actuation.num_seats,
180        ensures
181            weight == if self.actuation.allocation@[item as int] is Some {
182                self.actuation.allocation@[item as int]->Some_0
183            } else { 0 },
184    {
185        self.actuation.allocation[item].unwrap_or(0)
186    }
187
188    /// Executable selected-membership projection.
189    pub fn contains_exec(&self, item: usize) -> (selected: bool)
190        requires self.inv(),
191        ensures selected == self.contains(item),
192    {
193        if item >= self.actuation.effects.len() {
194            false
195        } else {
196            self.actuation.is_actuated(item)
197        }
198    }
199
200    /// Couple ActuationPass.Actuate with Budget.TryAllocate(1).
201    pub fn sample(&mut self, item: usize)
202        requires
203            old(self).inv(),
204            item < old(self).actuation.num_seats,
205            old(self).budget.allocated < old(self).budget.capacity,
206            old(self).actuation.allocation@[item as int] is Some,
207            old(self).actuation.effects@[item as int] is None,
208        ensures
209            final(self).inv(),
210            final(self).budget.capacity == old(self).budget.capacity,
211            final(self).budget.allocated == old(self).budget.allocated + 1,
212            final(self).actuation.allocation@ == old(self).actuation.allocation@,
213            final(self).actuation.effects@
214                == old(self).actuation.effects@.update(
215                    item as int,
216                    old(self).actuation.allocation@[item as int],
217                ),
218    {
219        let ghost prior_effects = self.actuation.effects@;
220        let _accepted = self.budget.try_allocate(1);
221        assert(_accepted);
222        self.actuation.actuate(item);
223        proof {
224            selected_count_update(
225                prior_effects,
226                item as int,
227                self.actuation.effects@[item as int],
228                prior_effects.len() as int,
229            );
230        }
231    }
232
233    /// Withdraw an unselected support item through ActuationPass.Deallocate.
234    pub fn zero(&mut self, item: usize) -> (accepted: bool)
235        requires old(self).inv(),
236        ensures
237            final(self).inv(),
238            accepted == (item < old(self).actuation.num_seats && !old(self).contains(item)),
239            final(self).budget == old(self).budget,
240            final(self).actuation.effects@ == old(self).actuation.effects@,
241            accepted && old(self).actuation.allocation@[item as int] is Some
242                ==> final(self).actuation.allocation@
243                    == old(self).actuation.allocation@.update(item as int, None),
244            !accepted ==> final(self).actuation.allocation@ == old(self).actuation.allocation@,
245    {
246        if item >= self.actuation.num_seats || self.contains_exec(item) {
247            return false;
248        }
249        if self.actuation.is_allocated(item) {
250            self.actuation.deallocate(item);
251        }
252        true
253    }
254
255    /// Weighted rejection over caller-supplied proposal and entropy.
256    pub fn draw_weighted(&mut self, item: usize, entropy: u64) -> (accepted: bool)
257        requires old(self).inv(), item < old(self).actuation.num_seats,
258        ensures final(self).inv(),
259    {
260        if self.budget.allocated >= self.budget.capacity {
261            return false;
262        }
263        let weight = self.weight(item);
264        if entropy >= weight || self.contains_exec(item) {
265            return false;
266        }
267        self.sample(item);
268        true
269    }
270
271    /// Uniform-support admission over a caller-supplied proposal.
272    pub fn draw_uniform(&mut self, item: usize) -> (accepted: bool)
273        requires old(self).inv(), item < old(self).actuation.num_seats,
274        ensures final(self).inv(),
275    {
276        if self.budget.allocated >= self.budget.capacity {
277            return false;
278        }
279        let weight = self.weight(item);
280        if weight == 0 || self.contains_exec(item) {
281            return false;
282        }
283        self.sample(item);
284        true
285    }
286}
287
288}