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//! | [`bodies`] | The other shape a domain can be: a countable number of things at places |
41//! | [`pose`] | Rigid motion — a rotation and a translation, and deliberately nothing more |
42//! | [`substance`] | Thermal, mechanical and acoustic properties of matter |
43//! | [`motion`] | Closed-form rigid motion and time gating |
44//! | [`rng`] | A deterministic generator, and the sampling built on it |
45//! | [`ensemble`] | Many independent samples in parallel, with an answer that does not depend on how many threads produced it |
46//! | [`transform`] | The discrete Fourier transform, accurate rather than fast |
47//! | [`vector`] | Basis construction and reflection — the vector maths no domain owns |
48//!
49//! [`scene`] here is **not** the `dualis-scene` crate, and the collision is worth naming. This
50//! module is where two domains *meet* — an [`Interface`] cut into faces and a [`Flux`] that
51//! knows which one it crossed — and it is kernel business because the audit runs on it. The
52//! crate one layer up is where a domain *sits*, which is a statement about the world and not
53//! about physics, and it is above every domain for that reason.
54//!
55//! # What a domain offers a layer above it
56//!
57//! Four optional accessors, none of which the kernel uses itself: [`ScalarField`] through
58//! `as_field` for a continuum, [`Bodies`] through `as_bodies` for a countable set, [`Reading`]
59//! through `readings` for the scalars a domain has when it has no picture, and `as_any` for a
60//! caller that knows the concrete type.
61//!
62//! They exist so that a layer which must visit every domain never has to name one. **All four
63//! are opt-in and default to nothing**, which is the hazard worth stating once: a domain that
64//! forgets is silently absent from every table and every picture rather than failing to
65//! compile. That has happened — four mechanics domains never opted into `as_any`, and an orbit
66//! scene ran, conserved, and drew nothing at all.
67//!
68//! # A domain, and the audit that checks it
69//!
70//! ```
71//! use dualis_core::conserved::quantity;
72//! use dualis_core::{Domain, Exchange, Kind, Ledger, Schedule, Simulation, Violation};
73//! use dualis_core::units::Time;
74//!
75//! /// A source that pays out a watt, and says so in its books.
76//! struct Lamp { paid: f64 }
77//! impl Domain for Lamp {
78//! fn name(&self) -> &str { "lamp" }
79//! fn kind(&self) -> Kind { Kind::QuasiStatic }
80//! fn step(&mut self, _t: Time, dt: Time, bus: &mut Exchange) -> Result<(), Violation> {
81//! let joules = 1.0 * dt.to_si();
82//! bus.publish(quantity::ENERGY, joules);
83//! self.paid += joules;
84//! Ok(())
85//! }
86//! // Negative: it is holding a debt, having handed the energy away.
87//! fn ledger(&self) -> Ledger { Ledger::new().with(quantity::ENERGY, -self.paid) }
88//! }
89//!
90//! /// A sink that takes whatever is offered and keeps it.
91//! struct Block { held: f64 }
92//! impl Domain for Block {
93//! fn name(&self) -> &str { "block" }
94//! fn step(&mut self, _t: Time, _dt: Time, bus: &mut Exchange) -> Result<(), Violation> {
95//! self.held += bus.take(quantity::ENERGY);
96//! Ok(())
97//! }
98//! fn ledger(&self) -> Ledger { Ledger::new().with(quantity::ENERGY, self.held) }
99//! // Opt in to being readable from outside. Without this, `domain_as` returns `None`:
100//! // the coupling never needs the concrete type, so it is not given away by default.
101//! fn as_any(&self) -> Option<&dyn std::any::Any> { Some(self) }
102//! fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> { Some(self) }
103//! }
104//!
105//! let mut sim = Simulation::new(Schedule::Staggered)
106//! .with(Lamp { paid: 0.0 })
107//! .with(Block { held: 0.0 });
108//! for _ in 0..10 {
109//! sim.advance(Time::ms(100.0)).expect("the books balance");
110//! }
111//!
112//! // A joule went across, and the two ledgers cancel because nothing was lost.
113//! let block: &Block = sim.domain_as("block").unwrap();
114//! assert!((block.held - 1.0).abs() < 1e-12);
115//! assert!(sim.ledger().get(quantity::ENERGY).unwrap().abs() < 1e-12);
116//! ```
117//!
118//! Had `Block` consumed only half of what was published, `advance` would have returned a
119//! [`Violation`] naming the channel rather than quietly losing the rest.
120//!
121//! Units come from `dualis-units` and are re-exported below, so a domain crate
122//! needs one dependency rather than two.
123
124// Every public item carries a doc comment. Denied rather than warned: a public physics API
125// whose `Length::mm` shows a blank summary in rustdoc is documented in the sense that a
126// paragraph exists somewhere, and not in the sense a reader needs.
127#![deny(missing_docs)]
128pub mod bodies;
129pub mod conserved;
130pub mod ensemble;
131pub mod field;
132pub mod integrator;
133pub mod motion;
134pub mod pose;
135pub mod rng;
136pub mod scene;
137pub mod sim;
138pub mod substance;
139pub mod transform;
140pub mod vector;
141
142pub use bodies::Bodies;
143pub use conserved::{audit, audit_with, Conserves, Ledger, Tolerances, Violation};
144pub use ensemble::{Ensemble, Estimate};
145pub use field::{ScalarField, VectorField};
146pub use integrator::{velocity_verlet, Dynamics, Integrator, Newtonian, State};
147pub use motion::{Motion, Strobe};
148pub use pose::Pose;
149pub use rng::Rng;
150pub use scene::{Flux, Interface};
151pub use sim::{Domain, Exchange, Kind, Reading, Report, Schedule, Simulation};
152pub use substance::Substance;
153pub use transform::{fft, fft2, fftshift, ifft, ifft2};
154pub use vector::{basis_for, oriented_against, reflect};
155
156/// Everything from `dualis-units`, so that `use dualis_core::units::*` is enough
157/// to write dimensioned physics.
158pub mod units {
159 pub use dualis_units::*;
160}