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    /// Default the absent ActuationPass allocation to zero support weight.
93    pub open spec fn support_weight(&self, item: usize) -> u64 {
94        if item < self.actuation.allocation.len()
95            && self.actuation.allocation@[item as int] is Some
96        {
97            self.actuation.allocation@[item as int]->Some_0
98        } else {
99            0
100        }
101    }
102
103    /// Exact admission predicate for a caller-proposed weighted draw.
104    pub open spec fn weighted_draw_enabled(&self, item: usize, entropy: u64) -> bool {
105        &&& item < self.actuation.num_seats
106        &&& self.budget.allocated < self.budget.capacity
107        &&& entropy < self.support_weight(item)
108        &&& !self.contains(item)
109    }
110
111    /// Exact admission predicate for a caller-proposed uniform draw.
112    pub open spec fn uniform_draw_enabled(&self, item: usize) -> bool {
113        &&& item < self.actuation.num_seats
114        &&& self.budget.allocated < self.budget.capacity
115        &&& self.support_weight(item) > 0
116        &&& !self.contains(item)
117    }
118
119    /// Every live ActuationPass allocation has positive support weight.
120    pub open spec fn support_domain(&self) -> bool {
121        forall|i: int| 0 <= i < self.actuation.allocation.len()
122            && #[trigger] self.actuation.allocation@[i] is Some
123            ==> self.actuation.allocation@[i]->Some_0 > 0
124    }
125
126    /// The two primitive states and their coupling are well formed.
127    pub open spec fn type_invariant(&self) -> bool {
128        &&& self.actuation.invariant()
129        &&& !self.actuation.complete
130        &&& self.support_domain()
131        &&& self.budget.safety_invariant()
132        &&& self.budget.reserved == 0
133        &&& self.budget.pending_eviction == 0
134        &&& self.budget.allocated as int
135            == selected_count(self.actuation.effects@, self.actuation.effects.len() as int)
136    }
137
138    /// Selected cardinality is owned by the Budget.
139    pub open spec fn bounded_sample(&self) -> bool {
140        self.budget.allocated <= self.budget.capacity
141    }
142
143    /// Every selected effect is tied to a positive live ActuationPass allocation.
144    pub open spec fn support_consistency(&self) -> bool {
145        forall|i: int| 0 <= i < self.actuation.effects.len()
146            && #[trigger] self.actuation.effects@[i] is Some
147            ==> self.actuation.allocation@[i] is Some
148                && self.actuation.allocation@[i]->Some_0 > 0
149    }
150
151    /// Full composition invariant.
152    pub open spec fn inv(&self) -> bool {
153        self.type_invariant() && self.bounded_sample() && self.support_consistency()
154    }
155
156    /// Build the ActuationPass support projection and an empty sample Budget.
157    pub fn new(distribution: Vec<u64>, sample_size: usize) -> (sampler: Self)
158        ensures
159            sampler.actuation.num_seats == distribution@.len(),
160            sampler.budget.capacity == sample_size as u64,
161            sampler.budget.allocated == 0,
162            sampler.budget.reserved == 0,
163            sampler.budget.pending_eviction == 0,
164            !sampler.actuation.complete,
165            forall|i: int| 0 <= i < distribution@.len() ==>
166                #[trigger] sampler.actuation.effects@[i] is None,
167            sampler.inv(),
168            forall|i: int| 0 <= i < distribution@.len() ==> {
169                let allocation = #[trigger] sampler.actuation.allocation@[i];
170                if distribution@[i] == 0 {
171                    allocation is None
172                } else {
173                    allocation == Some(distribution@[i])
174                }
175            },
176    {
177        let length = distribution.len();
178        let mut allocation: Vec<Option<u64>> = Vec::new();
179        let mut index: usize = 0;
180        while index < length
181            invariant
182                index <= length,
183                length == distribution@.len(),
184                allocation.len() == index,
185                forall|i: int| 0 <= i < index ==> {
186                    let value = #[trigger] allocation@[i];
187                    if distribution@[i] == 0 {
188                        value is None
189                    } else {
190                        value == Some(distribution@[i])
191                    }
192                },
193            decreases length - index,
194        {
195            let weight = distribution[index];
196            if weight == 0 {
197                allocation.push(None);
198            } else {
199                allocation.push(Some(weight));
200            }
201            index += 1;
202        }
203        let actuation = ActuationPass::new(allocation, length);
204        let budget = Budget::new(sample_size as u64);
205        proof { selected_count_none(actuation.effects@, actuation.effects.len() as int); }
206        Self { actuation, budget }
207    }
208
209    /// Read one support weight, mapping ActuationPass's absent allocation to zero.
210    pub fn weight(&self, item: usize) -> (weight: u64)
211        requires self.inv(), item < self.actuation.num_seats,
212        ensures weight == self.support_weight(item),
213    {
214        self.actuation.allocation[item].unwrap_or(0)
215    }
216
217    /// Executable selected-membership projection.
218    pub fn contains_exec(&self, item: usize) -> (selected: bool)
219        requires self.inv(),
220        ensures selected == self.contains(item),
221    {
222        if item >= self.actuation.effects.len() {
223            false
224        } else {
225            self.actuation.is_actuated(item)
226        }
227    }
228
229    /// Couple ActuationPass.Actuate with Budget.TryAllocate(1).
230    pub fn sample(&mut self, item: usize)
231        requires
232            old(self).inv(),
233            item < old(self).actuation.num_seats,
234            old(self).budget.allocated < old(self).budget.capacity,
235            old(self).actuation.allocation@[item as int] is Some,
236            old(self).actuation.effects@[item as int] is None,
237        ensures
238            final(self).inv(),
239            final(self).budget.capacity == old(self).budget.capacity,
240            final(self).budget.allocated == old(self).budget.allocated + 1,
241            final(self).actuation.allocation@ == old(self).actuation.allocation@,
242            final(self).actuation.effects@
243                == old(self).actuation.effects@.update(
244                    item as int,
245                    old(self).actuation.allocation@[item as int],
246                ),
247    {
248        let ghost prior_effects = self.actuation.effects@;
249        let _accepted = self.budget.try_allocate(1);
250        assert(_accepted);
251        self.actuation.actuate(item);
252        proof {
253            selected_count_update(
254                prior_effects,
255                item as int,
256                self.actuation.effects@[item as int],
257                prior_effects.len() as int,
258            );
259        }
260    }
261
262    /// Withdraw an unselected support item through ActuationPass.Deallocate.
263    pub fn zero(&mut self, item: usize) -> (accepted: bool)
264        requires old(self).inv(),
265        ensures
266            final(self).inv(),
267            accepted == (item < old(self).actuation.num_seats && !old(self).contains(item)),
268            final(self).budget == old(self).budget,
269            final(self).actuation.num_seats == old(self).actuation.num_seats,
270            final(self).actuation.complete == old(self).actuation.complete,
271            final(self).actuation.effects@ == old(self).actuation.effects@,
272            accepted ==> final(self).actuation.allocation@
273                == if old(self).actuation.allocation@[item as int] is Some {
274                    old(self).actuation.allocation@.update(item as int, None)
275                } else {
276                    old(self).actuation.allocation@
277                },
278            !accepted ==> final(self).actuation.allocation@ == old(self).actuation.allocation@,
279    {
280        if item >= self.actuation.num_seats || self.contains_exec(item) {
281            return false;
282        }
283        if self.actuation.is_allocated(item) {
284            self.actuation.deallocate(item);
285        }
286        true
287    }
288
289    /// Weighted rejection over caller-supplied proposal and entropy.
290    pub fn draw_weighted(&mut self, item: usize, entropy: u64) -> (accepted: bool)
291        requires old(self).inv(), item < old(self).actuation.num_seats,
292        ensures
293            final(self).inv(),
294            accepted == old(self).weighted_draw_enabled(item, entropy),
295            final(self).budget.capacity == old(self).budget.capacity,
296            final(self).budget.reserved == old(self).budget.reserved,
297            final(self).budget.pending_eviction == old(self).budget.pending_eviction,
298            final(self).actuation.num_seats == old(self).actuation.num_seats,
299            final(self).actuation.allocation@ == old(self).actuation.allocation@,
300            final(self).actuation.complete == old(self).actuation.complete,
301            accepted ==> {
302                &&& final(self).budget.allocated == old(self).budget.allocated + 1
303                &&& final(self).actuation.effects@
304                    == old(self).actuation.effects@.update(
305                        item as int,
306                        old(self).actuation.allocation@[item as int],
307                    )
308            },
309            !accepted ==> {
310                &&& final(self).budget.allocated == old(self).budget.allocated
311                &&& final(self).actuation.effects@ == old(self).actuation.effects@
312            },
313    {
314        if self.budget.allocated >= self.budget.capacity {
315            return false;
316        }
317        let weight = self.weight(item);
318        if entropy >= weight || self.contains_exec(item) {
319            return false;
320        }
321        self.sample(item);
322        true
323    }
324
325    /// Uniform-support admission over a caller-supplied proposal.
326    pub fn draw_uniform(&mut self, item: usize) -> (accepted: bool)
327        requires old(self).inv(), item < old(self).actuation.num_seats,
328        ensures
329            final(self).inv(),
330            accepted == old(self).uniform_draw_enabled(item),
331            final(self).budget.capacity == old(self).budget.capacity,
332            final(self).budget.reserved == old(self).budget.reserved,
333            final(self).budget.pending_eviction == old(self).budget.pending_eviction,
334            final(self).actuation.num_seats == old(self).actuation.num_seats,
335            final(self).actuation.allocation@ == old(self).actuation.allocation@,
336            final(self).actuation.complete == old(self).actuation.complete,
337            accepted ==> {
338                &&& final(self).budget.allocated == old(self).budget.allocated + 1
339                &&& final(self).actuation.effects@
340                    == old(self).actuation.effects@.update(
341                        item as int,
342                        old(self).actuation.allocation@[item as int],
343                    )
344            },
345            !accepted ==> {
346                &&& final(self).budget.allocated == old(self).budget.allocated
347                &&& final(self).actuation.effects@ == old(self).actuation.effects@
348            },
349    {
350        if self.budget.allocated >= self.budget.capacity {
351            return false;
352        }
353        let weight = self.weight(item);
354        if weight == 0 || self.contains_exec(item) {
355            return false;
356        }
357        self.sample(item);
358        true
359    }
360}
361
362}