dualis_core/lib.rs
1//! dualis-core: the kernel a simulated world's physics is built on.
2//!
3//! This crate knows nothing about any particular physics. It knows that a
4//! quantity can vary over space and time, that a process must answer for what it
5//! conserves, that a system with no closed form has to be rolled forward, that
6//! matter has properties several domains need at once, and that several domains
7//! sharing a clock is a scheduling problem with real failure modes. What any of
8//! that is *about* — light, heat, contact, sound — belongs to a domain crate.
9//!
10//! That separation is the point. `dualis-optics` depends on this crate; this crate
11//! must never depend on it, or anything else that models a specific physics. If a
12//! new domain needs the kernel changed, the kernel was wrong.
13//!
14//! # The two invariants
15//!
16//! Both survive the generalisation, and both are now enforced rather than
17//! promised:
18//!
19//! - **Nothing is created or destroyed without being noticed.** A [`Ledger`] is
20//! what a process claims to hold and [`audit`] is the check; energy crossing
21//! between domains goes through [`Exchange`], which refuses to let a transfer
22//! silently lose some. This generalises what `SurfaceOptics` did for one
23//! quantity at one kind of boundary. Where a boundary is resolved into faces,
24//! the audit is per face — a redistribution that keeps the total but moves it
25//! to the wrong part of a mirror is the one bug a total-only check cannot see.
26//! - **Nothing is random.** [`Rng::for_index`] gives every piece of work its own
27//! stateless stream, so a parallel simulation is still bit-reproducible — which
28//! is when the guarantee starts to matter, and when a single shared generator
29//! would have quietly lost it.
30//!
31//! # What is here
32//!
33//! | Module | |
34//! | --- | --- |
35//! | [`conserved`] | Conservation as an audit: ledgers, violations, tolerances |
36//! | [`integrator`] | Fixed-step time evolution, and why symplectic beats accurate |
37//! | [`sim`] | Several domains on one clock: quasi-static, multirate, iterative coupling |
38//! | [`scene`] | Where two domains meet: shared boundaries, and flux that knows its place |
39//! | [`field`] | Scalar and vector fields, with gradient, divergence, curl, Laplacian |
40//! | [`substance`] | Thermal, mechanical and acoustic properties of matter |
41//! | [`motion`] | Closed-form rigid motion and time gating |
42//! | [`rng`] | A deterministic generator, and the sampling built on it |
43//! | [`transform`] | The discrete Fourier transform, accurate rather than fast |
44//! | [`vector`] | Basis construction and reflection — the vector maths no domain owns |
45//!
46//! # A domain, and the audit that checks it
47//!
48//! ```
49//! use dualis_core::conserved::quantity;
50//! use dualis_core::{Domain, Exchange, Kind, Ledger, Schedule, Simulation, Violation};
51//! use dualis_core::units::Time;
52//!
53//! /// A source that pays out a watt, and says so in its books.
54//! struct Lamp { paid: f64 }
55//! impl Domain for Lamp {
56//! fn name(&self) -> &str { "lamp" }
57//! fn kind(&self) -> Kind { Kind::QuasiStatic }
58//! fn step(&mut self, _t: Time, dt: Time, bus: &mut Exchange) -> Result<(), Violation> {
59//! let joules = 1.0 * dt.to_si();
60//! bus.publish(quantity::ENERGY, joules);
61//! self.paid += joules;
62//! Ok(())
63//! }
64//! // Negative: it is holding a debt, having handed the energy away.
65//! fn ledger(&self) -> Ledger { Ledger::new().with(quantity::ENERGY, -self.paid) }
66//! }
67//!
68//! /// A sink that takes whatever is offered and keeps it.
69//! struct Block { held: f64 }
70//! impl Domain for Block {
71//! fn name(&self) -> &str { "block" }
72//! fn step(&mut self, _t: Time, _dt: Time, bus: &mut Exchange) -> Result<(), Violation> {
73//! self.held += bus.take(quantity::ENERGY);
74//! Ok(())
75//! }
76//! fn ledger(&self) -> Ledger { Ledger::new().with(quantity::ENERGY, self.held) }
77//! // Opt in to being readable from outside. Without this, `domain_as` returns `None`:
78//! // the coupling never needs the concrete type, so it is not given away by default.
79//! fn as_any(&self) -> Option<&dyn std::any::Any> { Some(self) }
80//! fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> { Some(self) }
81//! }
82//!
83//! let mut sim = Simulation::new(Schedule::Staggered)
84//! .with(Lamp { paid: 0.0 })
85//! .with(Block { held: 0.0 });
86//! for _ in 0..10 {
87//! sim.advance(Time::ms(100.0)).expect("the books balance");
88//! }
89//!
90//! // A joule went across, and the two ledgers cancel because nothing was lost.
91//! let block: &Block = sim.domain_as("block").unwrap();
92//! assert!((block.held - 1.0).abs() < 1e-12);
93//! assert!(sim.ledger().get(quantity::ENERGY).unwrap().abs() < 1e-12);
94//! ```
95//!
96//! Had `Block` consumed only half of what was published, `advance` would have returned a
97//! [`Violation`] naming the channel rather than quietly losing the rest.
98//!
99//! Units come from `dualis-units` and are re-exported below, so a domain crate
100//! needs one dependency rather than two.
101
102// Every public item carries a doc comment. Denied rather than warned: a public physics API
103// whose `Length::mm` shows a blank summary in rustdoc is documented in the sense that a
104// paragraph exists somewhere, and not in the sense a reader needs.
105#![deny(missing_docs)]
106pub mod conserved;
107pub mod ensemble;
108pub mod field;
109pub mod integrator;
110pub mod motion;
111pub mod rng;
112pub mod scene;
113pub mod sim;
114pub mod substance;
115pub mod transform;
116pub mod vector;
117
118pub use conserved::{audit, Conserves, Ledger, Violation};
119pub use ensemble::{Ensemble, Estimate};
120pub use field::{ScalarField, VectorField};
121pub use integrator::{velocity_verlet, Dynamics, Integrator, Newtonian, State};
122pub use motion::{Motion, Strobe};
123pub use rng::Rng;
124pub use scene::{Flux, Interface};
125pub use sim::{Domain, Exchange, Kind, Report, Schedule, Simulation};
126pub use substance::Substance;
127pub use transform::{fft, fft2, fftshift, ifft, ifft2};
128pub use vector::{basis_for, oriented_against, reflect};
129
130/// Everything from `dualis-units`, so that `use dualis_core::units::*` is enough
131/// to write dimensioned physics.
132pub mod units {
133 pub use dualis_units::*;
134}