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}