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}