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
152impl FieldDef {
153    /// Validate this field definition for structural correctness.
154    ///
155    /// Returns `Ok(())` if valid, or an error description if any invariant
156    /// is violated. Checks:
157    /// - `Vector { dims: 0 }` is rejected (zero components is meaningless).
158    /// - `Categorical { n_values: 0 }` is rejected (zero categories is meaningless).
159    /// - If `bounds` is `Some((min, max))`, requires `min <= max` and both finite.
160    pub fn validate(&self) -> Result<(), String> {
161        match self.field_type {
162            FieldType::Vector { dims: 0 } => {
163                return Err(format!("field '{}': Vector dims must be > 0", self.name));
164            }
165            FieldType::Categorical { n_values: 0 } => {
166                return Err(format!(
167                    "field '{}': Categorical n_values must be > 0",
168                    self.name
169                ));
170            }
171            _ => {}
172        }
173        if let Some((min, max)) = self.bounds {
174            if !min.is_finite() || !max.is_finite() {
175                return Err(format!(
176                    "field '{}': bounds must be finite, got ({}, {})",
177                    self.name, min, max
178                ));
179            }
180            if min > max {
181                return Err(format!(
182                    "field '{}': bounds min ({}) must be <= max ({})",
183                    self.name, min, max
184                ));
185            }
186        }
187        Ok(())
188    }
189}
190
191/// A set of field IDs implemented as a dynamically-sized bitset.
192///
193/// Used by propagators to declare which fields they read and write,
194/// enabling the engine to validate the dependency graph and compute
195/// overlay resolution plans.
196///
197/// # Examples
198///
199/// ```
200/// use murk_core::{FieldSet, FieldId};
201///
202/// let mut set = FieldSet::empty();
203/// set.insert(FieldId(0));
204/// set.insert(FieldId(3));
205/// assert!(set.contains(FieldId(0)));
206/// assert!(!set.contains(FieldId(1)));
207///
208/// // Collect all IDs.
209/// let ids: Vec<_> = set.iter().collect();
210/// assert_eq!(ids, vec![FieldId(0), FieldId(3)]);
211/// ```
212#[derive(Clone, Debug)]
213pub struct FieldSet {
214    bits: Vec<u64>,
215}
216
217impl FieldSet {
218    const BITS_PER_WORD: usize = 64;
219
220    /// Create an empty field set.
221    pub fn empty() -> Self {
222        Self { bits: Vec::new() }
223    }
224
225    /// Insert a field ID into the set.
226    pub fn insert(&mut self, field: FieldId) {
227        let word = field.0 as usize / Self::BITS_PER_WORD;
228        let bit = field.0 as usize % Self::BITS_PER_WORD;
229        if word >= self.bits.len() {
230            self.bits.resize(word + 1, 0);
231        }
232        self.bits[word] |= 1u64 << bit;
233    }
234
235    /// Check whether the set contains a field ID.
236    pub fn contains(&self, field: FieldId) -> bool {
237        let word = field.0 as usize / Self::BITS_PER_WORD;
238        let bit = field.0 as usize % Self::BITS_PER_WORD;
239        word < self.bits.len() && (self.bits[word] & (1u64 << bit)) != 0
240    }
241
242    /// Return the union of two sets (`self | other`).
243    ///
244    /// # Examples
245    ///
246    /// ```
247    /// use murk_core::{FieldSet, FieldId};
248    ///
249    /// let a: FieldSet = [FieldId(0), FieldId(1)].into_iter().collect();
250    /// let b: FieldSet = [FieldId(1), FieldId(2)].into_iter().collect();
251    /// let u = a.union(&b);
252    /// assert_eq!(u.len(), 3);
253    /// assert!(u.contains(FieldId(0)));
254    /// assert!(u.contains(FieldId(1)));
255    /// assert!(u.contains(FieldId(2)));
256    /// ```
257    #[must_use]
258    pub fn union(&self, other: &Self) -> Self {
259        let max_len = self.bits.len().max(other.bits.len());
260        let mut bits = Vec::with_capacity(max_len);
261        for i in 0..max_len {
262            let a = self.bits.get(i).copied().unwrap_or(0);
263            let b = other.bits.get(i).copied().unwrap_or(0);
264            bits.push(a | b);
265        }
266        Self { bits }
267    }
268
269    /// Return the intersection of two sets (`self & other`).
270    ///
271    /// # Examples
272    ///
273    /// ```
274    /// use murk_core::{FieldSet, FieldId};
275    ///
276    /// let a: FieldSet = [FieldId(0), FieldId(1)].into_iter().collect();
277    /// let b: FieldSet = [FieldId(1), FieldId(2)].into_iter().collect();
278    /// let inter = a.intersection(&b);
279    /// assert_eq!(inter.len(), 1);
280    /// assert!(inter.contains(FieldId(1)));
281    /// ```
282    #[must_use]
283    pub fn intersection(&self, other: &Self) -> Self {
284        let min_len = self.bits.len().min(other.bits.len());
285        let mut bits = Vec::with_capacity(min_len);
286        for i in 0..min_len {
287            bits.push(self.bits[i] & other.bits[i]);
288        }
289        while bits.last() == Some(&0) {
290            bits.pop();
291        }
292        Self { bits }
293    }
294
295    /// Return the set difference (`self - other`): elements in `self` but not `other`.
296    ///
297    /// # Examples
298    ///
299    /// ```
300    /// use murk_core::{FieldSet, FieldId};
301    ///
302    /// let a: FieldSet = [FieldId(0), FieldId(1), FieldId(2)].into_iter().collect();
303    /// let b: FieldSet = [FieldId(1)].into_iter().collect();
304    /// let diff = a.difference(&b);
305    /// assert_eq!(diff.len(), 2);
306    /// assert!(diff.contains(FieldId(0)));
307    /// assert!(!diff.contains(FieldId(1)));
308    /// assert!(diff.contains(FieldId(2)));
309    /// ```
310    #[must_use]
311    pub fn difference(&self, other: &Self) -> Self {
312        let mut bits = Vec::with_capacity(self.bits.len());
313        for i in 0..self.bits.len() {
314            let b = other.bits.get(i).copied().unwrap_or(0);
315            bits.push(self.bits[i] & !b);
316        }
317        while bits.last() == Some(&0) {
318            bits.pop();
319        }
320        Self { bits }
321    }
322
323    /// Check whether `self` is a subset of `other`.
324    #[must_use]
325    pub fn is_subset(&self, other: &Self) -> bool {
326        for i in 0..self.bits.len() {
327            let b = other.bits.get(i).copied().unwrap_or(0);
328            if self.bits[i] & !b != 0 {
329                return false;
330            }
331        }
332        true
333    }
334
335    /// Returns `true` if the set contains no fields.
336    #[must_use]
337    pub fn is_empty(&self) -> bool {
338        self.bits.iter().all(|&w| w == 0)
339    }
340
341    /// Returns the number of fields in the set.
342    #[must_use]
343    pub fn len(&self) -> usize {
344        self.bits.iter().map(|w| w.count_ones() as usize).sum()
345    }
346
347    /// Iterate over the field IDs in the set, in ascending order.
348    pub fn iter(&self) -> FieldSetIter<'_> {
349        FieldSetIter {
350            bits: &self.bits,
351            word_idx: 0,
352            bit_idx: 0,
353        }
354    }
355}
356
357impl PartialEq for FieldSet {
358    fn eq(&self, other: &Self) -> bool {
359        let max_len = self.bits.len().max(other.bits.len());
360        for i in 0..max_len {
361            let a = self.bits.get(i).copied().unwrap_or(0);
362            let b = other.bits.get(i).copied().unwrap_or(0);
363            if a != b {
364                return false;
365            }
366        }
367        true
368    }
369}
370
371impl Eq for FieldSet {}
372
373impl FromIterator<FieldId> for FieldSet {
374    fn from_iter<I: IntoIterator<Item = FieldId>>(iter: I) -> Self {
375        let mut set = Self::empty();
376        for field in iter {
377            set.insert(field);
378        }
379        set
380    }
381}
382
383impl<'a> IntoIterator for &'a FieldSet {
384    type Item = FieldId;
385    type IntoIter = FieldSetIter<'a>;
386
387    fn into_iter(self) -> Self::IntoIter {
388        self.iter()
389    }
390}
391
392/// Iterator over field IDs in a [`FieldSet`], yielding IDs in ascending order.
393pub struct FieldSetIter<'a> {
394    bits: &'a [u64],
395    word_idx: usize,
396    bit_idx: usize,
397}
398
399impl Iterator for FieldSetIter<'_> {
400    type Item = FieldId;
401
402    fn next(&mut self) -> Option<Self::Item> {
403        while self.word_idx < self.bits.len() {
404            let word = self.bits[self.word_idx];
405            while self.bit_idx < 64 {
406                let bit = self.bit_idx;
407                self.bit_idx += 1;
408                if word & (1u64 << bit) != 0 {
409                    return Some(FieldId((self.word_idx * 64 + bit) as u32));
410                }
411            }
412            self.word_idx += 1;
413            self.bit_idx = 0;
414        }
415        None
416    }
417}
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422    use proptest::prelude::*;
423
424    fn arb_field_set() -> impl Strategy<Value = FieldSet> {
425        prop::collection::vec(0u32..128, 0..32)
426            .prop_map(|ids| ids.into_iter().map(FieldId).collect::<FieldSet>())
427    }
428
429    proptest! {
430        #[test]
431        fn union_commutative(a in arb_field_set(), b in arb_field_set()) {
432            prop_assert_eq!(a.union(&b), b.union(&a));
433        }
434
435        #[test]
436        fn intersection_commutative(a in arb_field_set(), b in arb_field_set()) {
437            prop_assert_eq!(a.intersection(&b), b.intersection(&a));
438        }
439
440        #[test]
441        fn union_associative(
442            a in arb_field_set(),
443            b in arb_field_set(),
444            c in arb_field_set(),
445        ) {
446            prop_assert_eq!(a.union(&b).union(&c), a.union(&b.union(&c)));
447        }
448
449        #[test]
450        fn intersection_associative(
451            a in arb_field_set(),
452            b in arb_field_set(),
453            c in arb_field_set(),
454        ) {
455            prop_assert_eq!(
456                a.intersection(&b).intersection(&c),
457                a.intersection(&b.intersection(&c))
458            );
459        }
460
461        #[test]
462        fn union_identity(a in arb_field_set()) {
463            prop_assert_eq!(a.union(&FieldSet::empty()), a.clone());
464        }
465
466        #[test]
467        fn union_idempotent(a in arb_field_set()) {
468            prop_assert_eq!(a.union(&a), a.clone());
469        }
470
471        #[test]
472        fn intersection_idempotent(a in arb_field_set()) {
473            prop_assert_eq!(a.intersection(&a), a.clone());
474        }
475
476        #[test]
477        fn intersection_with_empty(a in arb_field_set()) {
478            prop_assert_eq!(a.intersection(&FieldSet::empty()), FieldSet::empty());
479        }
480
481        #[test]
482        fn difference_removes_common(a in arb_field_set(), b in arb_field_set()) {
483            let diff = a.difference(&b);
484            for field in diff.iter() {
485                prop_assert!(a.contains(field), "diff element {field:?} not in a");
486                prop_assert!(!b.contains(field), "diff element {field:?} in b");
487            }
488        }
489
490        #[test]
491        fn distributive_intersection_over_union(
492            a in arb_field_set(),
493            b in arb_field_set(),
494            c in arb_field_set(),
495        ) {
496            prop_assert_eq!(
497                a.intersection(&b.union(&c)),
498                a.intersection(&b).union(&a.intersection(&c))
499            );
500        }
501
502        #[test]
503        fn subset_reflexive(a in arb_field_set()) {
504            prop_assert!(a.is_subset(&a));
505        }
506
507        #[test]
508        fn empty_is_subset(a in arb_field_set()) {
509            prop_assert!(FieldSet::empty().is_subset(&a));
510        }
511
512        #[test]
513        fn insert_contains(id in 0u32..256) {
514            let mut set = FieldSet::empty();
515            set.insert(FieldId(id));
516            prop_assert!(set.contains(FieldId(id)));
517            prop_assert_eq!(set.len(), 1);
518        }
519
520        #[test]
521        fn len_matches_iter_count(a in arb_field_set()) {
522            prop_assert_eq!(a.len(), a.iter().count());
523        }
524    }
525}