Skip to main content

murk_core/
field.rs

1//! Field definitions, types, and the [`FieldSet`] bitset.
2
3use crate::id::FieldId;
4
5/// Classification of a field's data type.
6///
7/// # Examples
8///
9/// ```
10/// use murk_core::FieldType;
11///
12/// assert_eq!(FieldType::Scalar.components(), 1);
13/// assert_eq!(FieldType::Vector { dims: 3 }.components(), 3);
14/// assert_eq!(FieldType::Categorical { n_values: 10 }.components(), 1);
15/// ```
16#[derive(Clone, Debug, PartialEq, Eq)]
17pub enum FieldType {
18    /// A single floating-point value per cell.
19    Scalar,
20    /// A fixed-size vector of floating-point values per cell.
21    Vector {
22        /// Number of components in the vector (e.g., 3 for velocity).
23        dims: u32,
24    },
25    /// A categorical (discrete) value per cell, stored as a single f32 index.
26    Categorical {
27        /// Number of possible categories.
28        n_values: u32,
29    },
30}
31
32impl FieldType {
33    /// Returns the number of f32 storage slots this field type requires per cell.
34    pub fn components(&self) -> u32 {
35        match self {
36            Self::Scalar => 1,
37            Self::Vector { dims } => *dims,
38            Self::Categorical { .. } => 1,
39        }
40    }
41}
42
43/// Boundary behavior when field values exceed declared bounds.
44///
45/// # Examples
46///
47/// ```
48/// use murk_core::BoundaryBehavior;
49///
50/// let behaviors = [
51///     BoundaryBehavior::Clamp,
52///     BoundaryBehavior::Reflect,
53///     BoundaryBehavior::Absorb,
54///     BoundaryBehavior::Wrap,
55/// ];
56///
57/// // All four variants are distinct.
58/// for (i, a) in behaviors.iter().enumerate() {
59///     for (j, b) in behaviors.iter().enumerate() {
60///         assert_eq!(i == j, a == b);
61///     }
62/// }
63///
64/// // Copy semantics.
65/// let a = BoundaryBehavior::Wrap;
66/// let b = a;
67/// assert_eq!(a, b);
68/// ```
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub enum BoundaryBehavior {
71    /// Clamp the value to the nearest bound.
72    Clamp,
73    /// Reflect the value off the bound.
74    Reflect,
75    /// Absorb at the boundary (value is set to the bound).
76    Absorb,
77    /// Wrap around to the opposite bound.
78    Wrap,
79}
80
81/// How a field's allocation is managed across ticks.
82///
83/// # Examples
84///
85/// ```
86/// use murk_core::FieldMutability;
87///
88/// // Static fields are shared across all snapshots.
89/// let m = FieldMutability::Static;
90/// assert_eq!(m, FieldMutability::Static);
91///
92/// // PerTick fields get a new allocation each tick.
93/// assert_ne!(FieldMutability::PerTick, FieldMutability::Sparse);
94/// ```
95#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub enum FieldMutability {
97    /// Generation 0 forever. Shared across all snapshots and vectorized envs.
98    Static,
99    /// New allocation each tick if modified. Per-generation.
100    PerTick,
101    /// New allocation only when modified. Shared until mutation.
102    Sparse,
103}
104
105/// Definition of a field registered in a simulation world.
106///
107/// Fields are the fundamental unit of per-cell state. Each field has a type,
108/// mutability class, optional bounds, and boundary behavior. Fields are
109/// registered at world creation; `FieldId` is the index into the field list.
110///
111/// # Examples
112///
113/// ```
114/// use murk_core::{FieldDef, FieldType, FieldMutability, BoundaryBehavior};
115///
116/// // A scalar field that is reallocated every tick.
117/// let heat = FieldDef {
118///     name: "heat".into(),
119///     field_type: FieldType::Scalar,
120///     mutability: FieldMutability::PerTick,
121///     units: Some("kelvin".into()),
122///     bounds: Some((0.0, 1000.0)),
123///     boundary_behavior: BoundaryBehavior::Clamp,
124/// };
125///
126/// // A 3D velocity vector allocated once (static terrain data).
127/// let velocity = FieldDef {
128///     name: "wind".into(),
129///     field_type: FieldType::Vector { dims: 3 },
130///     mutability: FieldMutability::Static,
131///     units: None,
132///     bounds: None,
133///     boundary_behavior: BoundaryBehavior::Clamp,
134/// };
135/// ```
136#[derive(Clone, Debug, PartialEq)]
137pub struct FieldDef {
138    /// Human-readable name for debugging and logging.
139    pub name: String,
140    /// Data type and dimensionality.
141    pub field_type: FieldType,
142    /// Allocation strategy across ticks.
143    pub mutability: FieldMutability,
144    /// Optional unit annotation (e.g., `"meters/sec"`).
145    pub units: Option<String>,
146    /// Optional `(min, max)` bounds for field values.
147    pub bounds: Option<(f32, f32)>,
148    /// Behavior when values exceed declared bounds.
149    pub boundary_behavior: BoundaryBehavior,
150}
151
152/// A set of field IDs implemented as a dynamically-sized bitset.
153///
154/// Used by propagators to declare which fields they read and write,
155/// enabling the engine to validate the dependency graph and compute
156/// overlay resolution plans.
157///
158/// # Examples
159///
160/// ```
161/// use murk_core::{FieldSet, FieldId};
162///
163/// let mut set = FieldSet::empty();
164/// set.insert(FieldId(0));
165/// set.insert(FieldId(3));
166/// assert!(set.contains(FieldId(0)));
167/// assert!(!set.contains(FieldId(1)));
168///
169/// // Collect all IDs.
170/// let ids: Vec<_> = set.iter().collect();
171/// assert_eq!(ids, vec![FieldId(0), FieldId(3)]);
172/// ```
173#[derive(Clone, Debug)]
174pub struct FieldSet {
175    bits: Vec<u64>,
176}
177
178impl FieldSet {
179    const BITS_PER_WORD: usize = 64;
180
181    /// Create an empty field set.
182    pub fn empty() -> Self {
183        Self { bits: Vec::new() }
184    }
185
186    /// Insert a field ID into the set.
187    pub fn insert(&mut self, field: FieldId) {
188        let word = field.0 as usize / Self::BITS_PER_WORD;
189        let bit = field.0 as usize % Self::BITS_PER_WORD;
190        if word >= self.bits.len() {
191            self.bits.resize(word + 1, 0);
192        }
193        self.bits[word] |= 1u64 << bit;
194    }
195
196    /// Check whether the set contains a field ID.
197    pub fn contains(&self, field: FieldId) -> bool {
198        let word = field.0 as usize / Self::BITS_PER_WORD;
199        let bit = field.0 as usize % Self::BITS_PER_WORD;
200        word < self.bits.len() && (self.bits[word] & (1u64 << bit)) != 0
201    }
202
203    /// Return the union of two sets (`self | other`).
204    ///
205    /// # Examples
206    ///
207    /// ```
208    /// use murk_core::{FieldSet, FieldId};
209    ///
210    /// let a: FieldSet = [FieldId(0), FieldId(1)].into_iter().collect();
211    /// let b: FieldSet = [FieldId(1), FieldId(2)].into_iter().collect();
212    /// let u = a.union(&b);
213    /// assert_eq!(u.len(), 3);
214    /// assert!(u.contains(FieldId(0)));
215    /// assert!(u.contains(FieldId(1)));
216    /// assert!(u.contains(FieldId(2)));
217    /// ```
218    pub fn union(&self, other: &Self) -> Self {
219        let max_len = self.bits.len().max(other.bits.len());
220        let mut bits = Vec::with_capacity(max_len);
221        for i in 0..max_len {
222            let a = self.bits.get(i).copied().unwrap_or(0);
223            let b = other.bits.get(i).copied().unwrap_or(0);
224            bits.push(a | b);
225        }
226        Self { bits }
227    }
228
229    /// Return the intersection of two sets (`self & other`).
230    ///
231    /// # Examples
232    ///
233    /// ```
234    /// use murk_core::{FieldSet, FieldId};
235    ///
236    /// let a: FieldSet = [FieldId(0), FieldId(1)].into_iter().collect();
237    /// let b: FieldSet = [FieldId(1), FieldId(2)].into_iter().collect();
238    /// let inter = a.intersection(&b);
239    /// assert_eq!(inter.len(), 1);
240    /// assert!(inter.contains(FieldId(1)));
241    /// ```
242    pub fn intersection(&self, other: &Self) -> Self {
243        let min_len = self.bits.len().min(other.bits.len());
244        let mut bits = Vec::with_capacity(min_len);
245        for i in 0..min_len {
246            bits.push(self.bits[i] & other.bits[i]);
247        }
248        while bits.last() == Some(&0) {
249            bits.pop();
250        }
251        Self { bits }
252    }
253
254    /// Return the set difference (`self - other`): elements in `self` but not `other`.
255    ///
256    /// # Examples
257    ///
258    /// ```
259    /// use murk_core::{FieldSet, FieldId};
260    ///
261    /// let a: FieldSet = [FieldId(0), FieldId(1), FieldId(2)].into_iter().collect();
262    /// let b: FieldSet = [FieldId(1)].into_iter().collect();
263    /// let diff = a.difference(&b);
264    /// assert_eq!(diff.len(), 2);
265    /// assert!(diff.contains(FieldId(0)));
266    /// assert!(!diff.contains(FieldId(1)));
267    /// assert!(diff.contains(FieldId(2)));
268    /// ```
269    pub fn difference(&self, other: &Self) -> Self {
270        let mut bits = Vec::with_capacity(self.bits.len());
271        for i in 0..self.bits.len() {
272            let b = other.bits.get(i).copied().unwrap_or(0);
273            bits.push(self.bits[i] & !b);
274        }
275        while bits.last() == Some(&0) {
276            bits.pop();
277        }
278        Self { bits }
279    }
280
281    /// Check whether `self` is a subset of `other`.
282    pub fn is_subset(&self, other: &Self) -> bool {
283        for i in 0..self.bits.len() {
284            let b = other.bits.get(i).copied().unwrap_or(0);
285            if self.bits[i] & !b != 0 {
286                return false;
287            }
288        }
289        true
290    }
291
292    /// Returns `true` if the set contains no fields.
293    pub fn is_empty(&self) -> bool {
294        self.bits.iter().all(|&w| w == 0)
295    }
296
297    /// Returns the number of fields in the set.
298    pub fn len(&self) -> usize {
299        self.bits.iter().map(|w| w.count_ones() as usize).sum()
300    }
301
302    /// Iterate over the field IDs in the set, in ascending order.
303    pub fn iter(&self) -> FieldSetIter<'_> {
304        FieldSetIter {
305            bits: &self.bits,
306            word_idx: 0,
307            bit_idx: 0,
308        }
309    }
310}
311
312impl PartialEq for FieldSet {
313    fn eq(&self, other: &Self) -> bool {
314        let max_len = self.bits.len().max(other.bits.len());
315        for i in 0..max_len {
316            let a = self.bits.get(i).copied().unwrap_or(0);
317            let b = other.bits.get(i).copied().unwrap_or(0);
318            if a != b {
319                return false;
320            }
321        }
322        true
323    }
324}
325
326impl Eq for FieldSet {}
327
328impl FromIterator<FieldId> for FieldSet {
329    fn from_iter<I: IntoIterator<Item = FieldId>>(iter: I) -> Self {
330        let mut set = Self::empty();
331        for field in iter {
332            set.insert(field);
333        }
334        set
335    }
336}
337
338impl<'a> IntoIterator for &'a FieldSet {
339    type Item = FieldId;
340    type IntoIter = FieldSetIter<'a>;
341
342    fn into_iter(self) -> Self::IntoIter {
343        self.iter()
344    }
345}
346
347/// Iterator over field IDs in a [`FieldSet`], yielding IDs in ascending order.
348pub struct FieldSetIter<'a> {
349    bits: &'a [u64],
350    word_idx: usize,
351    bit_idx: usize,
352}
353
354impl Iterator for FieldSetIter<'_> {
355    type Item = FieldId;
356
357    fn next(&mut self) -> Option<Self::Item> {
358        while self.word_idx < self.bits.len() {
359            let word = self.bits[self.word_idx];
360            while self.bit_idx < 64 {
361                let bit = self.bit_idx;
362                self.bit_idx += 1;
363                if word & (1u64 << bit) != 0 {
364                    return Some(FieldId((self.word_idx * 64 + bit) as u32));
365                }
366            }
367            self.word_idx += 1;
368            self.bit_idx = 0;
369        }
370        None
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377    use proptest::prelude::*;
378
379    fn arb_field_set() -> impl Strategy<Value = FieldSet> {
380        prop::collection::vec(0u32..128, 0..32)
381            .prop_map(|ids| ids.into_iter().map(FieldId).collect::<FieldSet>())
382    }
383
384    proptest! {
385        #[test]
386        fn union_commutative(a in arb_field_set(), b in arb_field_set()) {
387            prop_assert_eq!(a.union(&b), b.union(&a));
388        }
389
390        #[test]
391        fn intersection_commutative(a in arb_field_set(), b in arb_field_set()) {
392            prop_assert_eq!(a.intersection(&b), b.intersection(&a));
393        }
394
395        #[test]
396        fn union_associative(
397            a in arb_field_set(),
398            b in arb_field_set(),
399            c in arb_field_set(),
400        ) {
401            prop_assert_eq!(a.union(&b).union(&c), a.union(&b.union(&c)));
402        }
403
404        #[test]
405        fn intersection_associative(
406            a in arb_field_set(),
407            b in arb_field_set(),
408            c in arb_field_set(),
409        ) {
410            prop_assert_eq!(
411                a.intersection(&b).intersection(&c),
412                a.intersection(&b.intersection(&c))
413            );
414        }
415
416        #[test]
417        fn union_identity(a in arb_field_set()) {
418            prop_assert_eq!(a.union(&FieldSet::empty()), a.clone());
419        }
420
421        #[test]
422        fn union_idempotent(a in arb_field_set()) {
423            prop_assert_eq!(a.union(&a), a.clone());
424        }
425
426        #[test]
427        fn intersection_idempotent(a in arb_field_set()) {
428            prop_assert_eq!(a.intersection(&a), a.clone());
429        }
430
431        #[test]
432        fn intersection_with_empty(a in arb_field_set()) {
433            prop_assert_eq!(a.intersection(&FieldSet::empty()), FieldSet::empty());
434        }
435
436        #[test]
437        fn difference_removes_common(a in arb_field_set(), b in arb_field_set()) {
438            let diff = a.difference(&b);
439            for field in diff.iter() {
440                prop_assert!(a.contains(field), "diff element {field:?} not in a");
441                prop_assert!(!b.contains(field), "diff element {field:?} in b");
442            }
443        }
444
445        #[test]
446        fn distributive_intersection_over_union(
447            a in arb_field_set(),
448            b in arb_field_set(),
449            c in arb_field_set(),
450        ) {
451            prop_assert_eq!(
452                a.intersection(&b.union(&c)),
453                a.intersection(&b).union(&a.intersection(&c))
454            );
455        }
456
457        #[test]
458        fn subset_reflexive(a in arb_field_set()) {
459            prop_assert!(a.is_subset(&a));
460        }
461
462        #[test]
463        fn empty_is_subset(a in arb_field_set()) {
464            prop_assert!(FieldSet::empty().is_subset(&a));
465        }
466
467        #[test]
468        fn insert_contains(id in 0u32..256) {
469            let mut set = FieldSet::empty();
470            set.insert(FieldId(id));
471            prop_assert!(set.contains(FieldId(id)));
472            prop_assert_eq!(set.len(), 1);
473        }
474
475        #[test]
476        fn len_matches_iter_count(a in arb_field_set()) {
477            prop_assert_eq!(a.len(), a.iter().count());
478        }
479    }
480}