Skip to main content

scientific_workflow/
system_state.rs

1//! Template-defined, heterogeneous scientific system states.
2//!
3//! This module is the complete public boundary for describing one scientific
4//! system at a particular time point. A program first loads a JSON template
5//! into [`SystemStateSchema`], constructs its initial blank [`SystemState`], and then
6//! moves concrete Rust payloads into and out of the declared fields.
7//!
8//! # Public workflow
9//!
10//! 1. Load and validate a template with [`SystemStateSchema::load_json_template`].
11//! 2. Create the initial state with [`SystemStateSchema::create_empty_state`].
12//! 3. Assemble payload types and owners with [`SystemState::insert_payload`].
13//! 4. Borrow, mutate, or extract payloads through [`SystemState`].
14//!    Coordinated kernels use [`SystemState::borrow_payloads`] or
15//!    [`SystemState::borrow_payloads_mut`] with matching type and field-name tuples.
16//! 5. Mutate time through [`SystemState::replace_simulation_time`] or
17//!    [`SystemState::advance_simulation_time`].
18//! 6. Create later blank states with
19//!    [`SystemState::clone_structure_without_payloads`].
20//!
21//! The template fixes field names, field order, and optional human-facing
22//! descriptions. It contains no Rust type or storage codec information.
23//! Individual payload slots may be empty, but callers cannot add, remove, or
24//! reorder fields after the template is loaded. First insertion binds a slot's
25//! concrete Rust type. That contract survives extraction and clearing and is
26//! inherited by blank states derived from an assembled instance.
27//!
28//! Every inserted payload implements Serde `Serialize`, `Clone`, `Send`, and
29//! `'static`. Serialization is supplied by the payload type itself; this
30//! module only retains a private borrowed erased view for the future storage
31//! encoder. It does not select JSON framing or perform IO.
32//!
33//! # Boundary
34//!
35//! `system_state` owns schema declaration, field indexing, and owned in-memory
36//! evolution of typed payloads. It does not own persistence formats, execution
37//! controls, scheduling, sampling decisions, artifact publication, or RNG
38//! provenance.
39//!
40//! # Ownership
41//!
42//! [`SystemState::insert_payload`] consumes a concrete payload without cloning it. An
43//! insertion into an empty slot returns `None`; replacement returns the
44//! previous payload as `Some(T)`, preserving its ownership instead of dropping
45//! it. A rejected insertion returns [`PayloadInsertError<T>`], from which the unchanged
46//! incoming payload can be recovered.
47//!
48//! [`SystemState::take_payload`] moves a stored payload back to the caller. Together,
49//! insertion and extraction allow large scientific allocations to cross the state
50//! boundary without copying their contents. Explicitly cloning a
51//! [`SystemState`] is intentionally different: it creates a new erased box and
52//! invokes each populated payload's `Clone` implementation. Clone depth is
53//! therefore defined by the concrete payload type.
54//!
55//! The public insertion contract deliberately makes replacement visible:
56//!
57//! ```no_run
58//! use scientific_workflow::system_state::{SystemStateSchema, SimulationTime};
59//!
60//! # fn example(spec: &SystemStateSchema) -> Result<(), Box<dyn std::error::Error>> {
61//! let mut state = spec.create_empty_state(SimulationTime::from_iteration(0));
62//!
63//! let previous = state.insert_payload("population", vec![1_u64, 2, 3])?;
64//! assert!(previous.is_none());
65//!
66//! let previous = state.insert_payload("population", vec![4_u64, 5, 6])?;
67//! assert_eq!(previous, Some(vec![1, 2, 3]));
68//!
69//! let time = state.advance_simulation_time(None)?;
70//! assert_eq!(time.iteration(), 1);
71//! # Ok(())
72//! # }
73//! ```
74//!
75//! Ignoring a successful replacement result would drop the displaced payload.
76//! Callers should bind or explicitly drop the returned `Option<T>` so that
77//! ownership disposal is intentional.
78//!
79//! # Encapsulation
80//!
81//! Runtime type erasure and boxing are private implementation details.
82//! Downstream crates interact only with concrete types through generic state
83//! methods. Template parsing representations, compact field indices, and
84//! name-to-slot lookup tables are likewise hidden behind the public types
85//! re-exported below.
86//!
87//! Type erasure remains limited to the private heterogeneous owner. Concrete
88//! payload types and runtime identities are retained, and serialization
89//! erasure is borrowed only when storage explicitly requests it.
90
91mod error;
92mod schema;
93mod state;
94mod value;
95
96pub use error::{PayloadInsertError, StateError};
97pub use schema::{StateFieldSchema, SystemStateSchema};
98#[doc(hidden)]
99pub use state::PayloadTuple;
100pub use state::{SimulationTime, SystemState};
101
102/// A state-bearing value from which persistent-recording layout can be derived.
103///
104/// Both [`SystemState`] and [`SystemStateSchema`] implement this trait. Passing
105/// a live state is the natural choice for a new recording; passing a schema is
106/// useful when a continuation writer must be configured before checkpoint
107/// reconstruction. Writer builders retain only the cheap shared schema handle.
108pub trait StateSchemaSource {
109    /// Returns the immutable schema that defines the state's field layout.
110    fn state_schema(&self) -> &SystemStateSchema;
111}
112
113impl StateSchemaSource for SystemState {
114    fn state_schema(&self) -> &SystemStateSchema {
115        self.schema()
116    }
117}
118
119impl StateSchemaSource for SystemStateSchema {
120    fn state_schema(&self) -> &SystemStateSchema {
121        self
122    }
123}