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