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