Skip to main content

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//! }
81//!
82//! let mut sim = Simulation::new(Schedule::Staggered)
83//!     .with(Lamp { paid: 0.0 })
84//!     .with(Block { held: 0.0 });
85//! for _ in 0..10 {
86//!     sim.advance(Time::ms(100.0)).expect("the books balance");
87//! }
88//!
89//! // A joule went across, and the two ledgers cancel because nothing was lost.
90//! let block: &Block = sim.domain_as("block").unwrap();
91//! assert!((block.held - 1.0).abs() < 1e-12);
92//! assert!(sim.ledger().get(quantity::ENERGY).unwrap().abs() < 1e-12);
93//! ```
94//!
95//! Had `Block` consumed only half of what was published, `advance` would have returned a
96//! [`Violation`] naming the channel rather than quietly losing the rest.
97//!
98//! Units come from `dualis-units` and are re-exported below, so a domain crate
99//! needs one dependency rather than two.
100
101// Every public item carries a doc comment. Denied rather than warned: a public physics API
102// whose `Length::mm` shows a blank summary in rustdoc is documented in the sense that a
103// paragraph exists somewhere, and not in the sense a reader needs.
104#![deny(missing_docs)]
105pub mod conserved;
106pub mod field;
107pub mod integrator;
108pub mod motion;
109pub mod rng;
110pub mod scene;
111pub mod sim;
112pub mod substance;
113pub mod transform;
114pub mod vector;
115
116pub use conserved::{audit, Conserves, Ledger, Violation};
117pub use field::{ScalarField, VectorField};
118pub use integrator::{velocity_verlet, Dynamics, Integrator, Newtonian, State};
119pub use motion::{Motion, Strobe};
120pub use rng::Rng;
121pub use scene::{Flux, Interface};
122pub use sim::{Domain, Exchange, Kind, Report, Schedule, Simulation};
123pub use substance::Substance;
124pub use transform::{fft, fft2, fftshift, ifft, ifft2};
125pub use vector::{basis_for, oriented_against, reflect};
126
127/// Everything from `dualis-units`, so that `use dualis_core::units::*` is enough
128/// to write dimensioned physics.
129pub mod units {
130    pub use dualis_units::*;
131}