smolpy-engine-rs 1.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,277 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "block-buffer"
7
+ version = "0.12.1"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
10
+ dependencies = [
11
+ "hybrid-array",
12
+ ]
13
+
14
+ [[package]]
15
+ name = "cfg-if"
16
+ version = "1.0.5"
17
+ source = "registry+https://github.com/rust-lang/crates.io-index"
18
+ checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
19
+
20
+ [[package]]
21
+ name = "const-oid"
22
+ version = "0.10.3"
23
+ source = "registry+https://github.com/rust-lang/crates.io-index"
24
+ checksum = "be95d4a884358a21a199689323c29e9256b4a1b315e22f183d155422041d3a9a"
25
+
26
+ [[package]]
27
+ name = "cpufeatures"
28
+ version = "0.3.1"
29
+ source = "registry+https://github.com/rust-lang/crates.io-index"
30
+ checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
31
+ dependencies = [
32
+ "libc",
33
+ ]
34
+
35
+ [[package]]
36
+ name = "crypto-common"
37
+ version = "0.2.2"
38
+ source = "registry+https://github.com/rust-lang/crates.io-index"
39
+ checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
40
+ dependencies = [
41
+ "hybrid-array",
42
+ ]
43
+
44
+ [[package]]
45
+ name = "digest"
46
+ version = "0.11.3"
47
+ source = "registry+https://github.com/rust-lang/crates.io-index"
48
+ checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
49
+ dependencies = [
50
+ "block-buffer",
51
+ "const-oid",
52
+ "crypto-common",
53
+ ]
54
+
55
+ [[package]]
56
+ name = "heck"
57
+ version = "0.5.0"
58
+ source = "registry+https://github.com/rust-lang/crates.io-index"
59
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
60
+
61
+ [[package]]
62
+ name = "hybrid-array"
63
+ version = "0.4.15"
64
+ source = "registry+https://github.com/rust-lang/crates.io-index"
65
+ checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17"
66
+ dependencies = [
67
+ "typenum",
68
+ ]
69
+
70
+ [[package]]
71
+ name = "itoa"
72
+ version = "1.0.19"
73
+ source = "registry+https://github.com/rust-lang/crates.io-index"
74
+ checksum = "3bd1574e617a9a6c1f8f8181bd7e841e1f20e4277607ef28945765188d6284d4"
75
+
76
+ [[package]]
77
+ name = "libc"
78
+ version = "0.2.191"
79
+ source = "registry+https://github.com/rust-lang/crates.io-index"
80
+ checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
81
+
82
+ [[package]]
83
+ name = "memchr"
84
+ version = "2.8.3"
85
+ source = "registry+https://github.com/rust-lang/crates.io-index"
86
+ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
87
+
88
+ [[package]]
89
+ name = "once_cell"
90
+ version = "1.21.4"
91
+ source = "registry+https://github.com/rust-lang/crates.io-index"
92
+ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
93
+
94
+ [[package]]
95
+ name = "portable-atomic"
96
+ version = "1.15.0"
97
+ source = "registry+https://github.com/rust-lang/crates.io-index"
98
+ checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
99
+
100
+ [[package]]
101
+ name = "proc-macro2"
102
+ version = "1.0.107"
103
+ source = "registry+https://github.com/rust-lang/crates.io-index"
104
+ checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
105
+ dependencies = [
106
+ "unicode-ident",
107
+ ]
108
+
109
+ [[package]]
110
+ name = "pyo3"
111
+ version = "0.30.1"
112
+ source = "registry+https://github.com/rust-lang/crates.io-index"
113
+ checksum = "229ca13b8a15bb0d1487b81ab975bfcd97513416499a8312011b6036e42e0de4"
114
+ dependencies = [
115
+ "libc",
116
+ "once_cell",
117
+ "portable-atomic",
118
+ "pyo3-build-config",
119
+ "pyo3-ffi",
120
+ "pyo3-macros",
121
+ ]
122
+
123
+ [[package]]
124
+ name = "pyo3-build-config"
125
+ version = "0.30.1"
126
+ source = "registry+https://github.com/rust-lang/crates.io-index"
127
+ checksum = "70130e2b6dd1ee589a8788026a429561ca5c40fa7a2fa7914c2282eddae24700"
128
+ dependencies = [
129
+ "target-lexicon",
130
+ ]
131
+
132
+ [[package]]
133
+ name = "pyo3-ffi"
134
+ version = "0.30.1"
135
+ source = "registry+https://github.com/rust-lang/crates.io-index"
136
+ checksum = "5e7ff73c555c286d212d4ce7e5fbda845a973c4f61bd5f160f5f2c3c2a04d1c4"
137
+ dependencies = [
138
+ "libc",
139
+ "pyo3-build-config",
140
+ ]
141
+
142
+ [[package]]
143
+ name = "pyo3-macros"
144
+ version = "0.30.1"
145
+ source = "registry+https://github.com/rust-lang/crates.io-index"
146
+ checksum = "c46d2e5862c7bb8207e3b639f713dda3c499933444784154c11d013c5a691b60"
147
+ dependencies = [
148
+ "proc-macro2",
149
+ "pyo3-macros-backend",
150
+ "quote",
151
+ "syn",
152
+ ]
153
+
154
+ [[package]]
155
+ name = "pyo3-macros-backend"
156
+ version = "0.30.1"
157
+ source = "registry+https://github.com/rust-lang/crates.io-index"
158
+ checksum = "38b89f066f2ae72a2aef52d1537ca2fcd104440ebfadee06e97aa90738a25b9b"
159
+ dependencies = [
160
+ "heck",
161
+ "proc-macro2",
162
+ "quote",
163
+ "syn",
164
+ ]
165
+
166
+ [[package]]
167
+ name = "quote"
168
+ version = "1.0.47"
169
+ source = "registry+https://github.com/rust-lang/crates.io-index"
170
+ checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
171
+ dependencies = [
172
+ "proc-macro2",
173
+ ]
174
+
175
+ [[package]]
176
+ name = "serde"
177
+ version = "1.0.229"
178
+ source = "registry+https://github.com/rust-lang/crates.io-index"
179
+ checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
180
+ dependencies = [
181
+ "serde_core",
182
+ ]
183
+
184
+ [[package]]
185
+ name = "serde_core"
186
+ version = "1.0.229"
187
+ source = "registry+https://github.com/rust-lang/crates.io-index"
188
+ checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
189
+ dependencies = [
190
+ "serde_derive",
191
+ ]
192
+
193
+ [[package]]
194
+ name = "serde_derive"
195
+ version = "1.0.229"
196
+ source = "registry+https://github.com/rust-lang/crates.io-index"
197
+ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
198
+ dependencies = [
199
+ "proc-macro2",
200
+ "quote",
201
+ "syn",
202
+ ]
203
+
204
+ [[package]]
205
+ name = "serde_json"
206
+ version = "1.0.154"
207
+ source = "registry+https://github.com/rust-lang/crates.io-index"
208
+ checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
209
+ dependencies = [
210
+ "itoa",
211
+ "memchr",
212
+ "serde",
213
+ "serde_core",
214
+ "zmij",
215
+ ]
216
+
217
+ [[package]]
218
+ name = "sha2"
219
+ version = "0.11.1"
220
+ source = "registry+https://github.com/rust-lang/crates.io-index"
221
+ checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19"
222
+ dependencies = [
223
+ "cfg-if",
224
+ "cpufeatures",
225
+ "digest",
226
+ ]
227
+
228
+ [[package]]
229
+ name = "smol-engine"
230
+ version = "1.8.0"
231
+ dependencies = [
232
+ "serde_json",
233
+ "sha2",
234
+ ]
235
+
236
+ [[package]]
237
+ name = "smolpy-engine-rs"
238
+ version = "1.8.0"
239
+ dependencies = [
240
+ "pyo3",
241
+ "smol-engine",
242
+ ]
243
+
244
+ [[package]]
245
+ name = "syn"
246
+ version = "3.0.10"
247
+ source = "registry+https://github.com/rust-lang/crates.io-index"
248
+ checksum = "4ae9ccbea3a7b0b09443036490c8f34a8b1b760fd345e5c0dae0aac718faea79"
249
+ dependencies = [
250
+ "proc-macro2",
251
+ "quote",
252
+ "unicode-ident",
253
+ ]
254
+
255
+ [[package]]
256
+ name = "target-lexicon"
257
+ version = "0.13.6"
258
+ source = "registry+https://github.com/rust-lang/crates.io-index"
259
+ checksum = "8c447be3a2abfa84b2d91680c56600bd6b2873866de27b36ec911be705e291da"
260
+
261
+ [[package]]
262
+ name = "typenum"
263
+ version = "1.20.1"
264
+ source = "registry+https://github.com/rust-lang/crates.io-index"
265
+ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
266
+
267
+ [[package]]
268
+ name = "unicode-ident"
269
+ version = "1.0.27"
270
+ source = "registry+https://github.com/rust-lang/crates.io-index"
271
+ checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
272
+
273
+ [[package]]
274
+ name = "zmij"
275
+ version = "1.0.23"
276
+ source = "registry+https://github.com/rust-lang/crates.io-index"
277
+ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
@@ -0,0 +1,13 @@
1
+ [workspace]
2
+ members = ["smol-engine", "smolpy-engine-rs"]
3
+ resolver = "2"
4
+
5
+ [workspace.package]
6
+ version = "1.8.0"
7
+ edition = "2021"
8
+ license = "MIT"
9
+ repository = "https://github.com/jwszolek/SMOLPy"
10
+
11
+ [profile.release]
12
+ lto = "fat"
13
+ codegen-units = 1
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.4
2
+ Name: smolpy-engine-rs
3
+ Version: 1.8.0
4
+ Classifier: License :: OSI Approved :: MIT License
5
+ Classifier: Programming Language :: Python :: 3
6
+ Classifier: Programming Language :: Rust
7
+ Classifier: Topic :: System :: Networking
8
+ Classifier: Topic :: Scientific/Engineering
9
+ Summary: Native Rust simulation engine for SMOLPy: SimPy's results bit for bit, about 40x faster
10
+ License: MIT
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
13
+ Project-URL: Documentation, https://jwszolek.github.io/SMOLPy/
14
+ Project-URL: Homepage, https://github.com/jwszolek/SMOLPy
15
+ Project-URL: Source, https://github.com/jwszolek/SMOLPy/tree/main/rust
16
+
17
+ # smolpy-engine-rs
18
+
19
+ The native simulation engine of [SMOLPy](https://github.com/jwszolek/SMOLPy), a network description language and discrete-event simulator for industrial networks. It is written in Rust and exposed to Python with PyO3 ([ADR-0008](https://jwszolek.github.io/SMOLPy/adr/0008-rust-des-engine-mvp/)).
20
+
21
+ ```bash
22
+ pip install smolpy smolpy-engine-rs
23
+ ```
24
+
25
+ ```python
26
+ result = net.simulate(duration=60_000, backend="rust")
27
+ ```
28
+
29
+ - **The same model as SimPy.** SimPy, SMOLPy's reference engine, and this engine implement the same written contract, [Model Semantics](https://jwszolek.github.io/SMOLPy/model-semantics/). On deterministic scenarios they give the same results bit for bit, down to the event count. On random traffic, which uses each engine's own generator, they agree statistically.
30
+ - **About 40x faster.**
31
+ - B1 at 10 M frames runs in 4.9 s, against 200.8 s on SimPy.
32
+ - A 10 201-node plant (B4) at 100 M frames runs in 5.2 min, against 106 min.
33
+ - **Scope so far.**
34
+ - Adapters, switches (both QoS modes, learning and converged MAC tables), links, and flows with every pattern and IMIX sizes.
35
+ - The metrics `throughput`, `latency`, `bytes_sent`, `bytes_received`, `queue_depth` and `utilization`.
36
+ - MQTT, Modbus TCP and PROFINET follow. A scenario that uses them is rejected before the run, with a message naming the element.
37
+ - **Versions.** The package is released together with `smolpy`, with the same version. It checks that it reads the Scenario IR version that `smolpy` writes.
38
+
39
+ One abi3 wheel per platform serves every Python from 3.10: Linux x86_64 and aarch64, macOS arm64 and x86_64, and Windows x86_64.
40
+
@@ -0,0 +1,23 @@
1
+ # smolpy-engine-rs
2
+
3
+ The native simulation engine of [SMOLPy](https://github.com/jwszolek/SMOLPy), a network description language and discrete-event simulator for industrial networks. It is written in Rust and exposed to Python with PyO3 ([ADR-0008](https://jwszolek.github.io/SMOLPy/adr/0008-rust-des-engine-mvp/)).
4
+
5
+ ```bash
6
+ pip install smolpy smolpy-engine-rs
7
+ ```
8
+
9
+ ```python
10
+ result = net.simulate(duration=60_000, backend="rust")
11
+ ```
12
+
13
+ - **The same model as SimPy.** SimPy, SMOLPy's reference engine, and this engine implement the same written contract, [Model Semantics](https://jwszolek.github.io/SMOLPy/model-semantics/). On deterministic scenarios they give the same results bit for bit, down to the event count. On random traffic, which uses each engine's own generator, they agree statistically.
14
+ - **About 40x faster.**
15
+ - B1 at 10 M frames runs in 4.9 s, against 200.8 s on SimPy.
16
+ - A 10 201-node plant (B4) at 100 M frames runs in 5.2 min, against 106 min.
17
+ - **Scope so far.**
18
+ - Adapters, switches (both QoS modes, learning and converged MAC tables), links, and flows with every pattern and IMIX sizes.
19
+ - The metrics `throughput`, `latency`, `bytes_sent`, `bytes_received`, `queue_depth` and `utilization`.
20
+ - MQTT, Modbus TCP and PROFINET follow. A scenario that uses them is rejected before the run, with a message naming the element.
21
+ - **Versions.** The package is released together with `smolpy`, with the same version. It checks that it reads the Scenario IR version that `smolpy` writes.
22
+
23
+ One abi3 wheel per platform serves every Python from 3.10: Linux x86_64 and aarch64, macOS arm64 and x86_64, and Windows x86_64.
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["maturin>=1.7,<2"]
3
+ build-backend = "maturin"
4
+
5
+ [project]
6
+ name = "smolpy-engine-rs"
7
+ description = "Native Rust simulation engine for SMOLPy: SimPy's results bit for bit, about 40x faster"
8
+ readme = "README.md"
9
+ requires-python = ">=3.10"
10
+ license = { text = "MIT" }
11
+ version = "1.8.0"
12
+ classifiers = [
13
+ "License :: OSI Approved :: MIT License",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Rust",
16
+ "Topic :: System :: Networking",
17
+ "Topic :: Scientific/Engineering",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/jwszolek/SMOLPy"
22
+ Documentation = "https://jwszolek.github.io/SMOLPy/"
23
+ Source = "https://github.com/jwszolek/SMOLPy/tree/main/rust"
24
+
25
+ [tool.maturin]
26
+ features = ["pyo3/extension-module"]
27
+ module-name = "smolpy_engine_rs"
28
+ manifest-path = "smolpy-engine-rs/Cargo.toml"
@@ -0,0 +1,12 @@
1
+ [package]
2
+ name = "smol-engine"
3
+ version.workspace = true
4
+ edition.workspace = true
5
+ license.workspace = true
6
+ repository.workspace = true
7
+ description = "SMOLPy's native discrete-event network simulation engine (ADR-0008)"
8
+ publish = false # shipped as the Python package smolpy-engine-rs, not on crates.io
9
+
10
+ [dependencies]
11
+ serde_json = { version = "1.0.154", features = ["arbitrary_precision"] }
12
+ sha2 = "0.11.1"
@@ -0,0 +1,29 @@
1
+ //! Ethernet frames on the wire (ADR-0010), as `smolpy.model.frames`.
2
+ //!
3
+ //! A frame runs from the destination MAC to the FCS, is at least 64 B and
4
+ //! carries an 802.1Q tag with a priority above 0. On the wire every frame is
5
+ //! preceded by a preamble and SFD (8 B) and followed by an inter-frame gap
6
+ //! (12 B).
7
+
8
+ pub const VLAN_TAG_BYTES: u64 = 4;
9
+ pub const PREAMBLE_SFD_BYTES: u64 = 8;
10
+ pub const IFG_BYTES: u64 = 12;
11
+ pub const WIRE_OVERHEAD_BYTES: u64 = PREAMBLE_SFD_BYTES + IFG_BYTES;
12
+
13
+ /// A flow frame of `size` bytes (untagged, FCS included), with the tag a
14
+ /// priority above 0 needs.
15
+ pub fn flow_frame_size(size: u64, priority: u8) -> u64 {
16
+ size + if priority > 0 { VLAN_TAG_BYTES } else { 0 }
17
+ }
18
+
19
+ #[cfg(test)]
20
+ mod tests {
21
+ use super::*;
22
+
23
+ #[test]
24
+ fn flow_frames() {
25
+ assert_eq!(flow_frame_size(100, 0), 100);
26
+ assert_eq!(flow_frame_size(100, 3), 104);
27
+ assert_eq!(WIRE_OVERHEAD_BYTES, 20);
28
+ }
29
+ }
@@ -0,0 +1,18 @@
1
+ //! SMOLPy's native discrete-event engine (ADR-0008): a second implementation
2
+ //! of the semantics contract (docs/model-semantics.md), with no Python
3
+ //! dependency. `smolpy-engine-rs` exposes it to Python.
4
+
5
+ pub mod frames;
6
+ pub mod rng;
7
+ pub mod scenario;
8
+ pub mod sim;
9
+ pub mod timebase;
10
+
11
+ pub use scenario::CAPABILITIES;
12
+ pub use sim::Outcome;
13
+
14
+ /// Read a Scenario IR JSON document and run it.
15
+ pub fn run_json(text: &str) -> Result<Outcome, String> {
16
+ let scenario = scenario::parse(text)?;
17
+ sim::run(&scenario)
18
+ }
@@ -0,0 +1,174 @@
1
+ //! Random streams (Model Semantics §6) and the distributions of the traffic
2
+ //! patterns.
3
+ //!
4
+ //! - **Streams.** Every traffic statement has a stable source id,
5
+ //! `"<kind>:<from>-><to>#<k>"`, and its generator is seeded from the first
6
+ //! 8 bytes, big-endian, of SHA-256 of `"<run seed>:<source id>"`, exactly
7
+ //! as `smolpy.model.streams`.
8
+ //! - **Generator.** The engine's own: xoshiro256** seeded through SplitMix64.
9
+ //! Its draws differ from SimPy's `random.Random`, so stochastic scenarios
10
+ //! are compared statistically (ADR-0009).
11
+ //! - **Distributions** use the formulas of Python's `random` module, so the
12
+ //! two engines draw from the same distributions.
13
+
14
+ use sha2::{Digest, Sha256};
15
+
16
+ /// The stream seed of a traffic statement.
17
+ pub fn stream_seed(run_seed: u64, source_id: &str) -> u64 {
18
+ let digest = Sha256::digest(format!("{run_seed}:{source_id}").as_bytes());
19
+ u64::from_be_bytes(digest[..8].try_into().unwrap())
20
+ }
21
+
22
+ /// xoshiro256** (Blackman and Vigna), seeded through SplitMix64.
23
+ pub struct Rng {
24
+ s: [u64; 4],
25
+ }
26
+
27
+ impl Rng {
28
+ pub fn new(seed: u64) -> Rng {
29
+ let mut x = seed;
30
+ let mut next = || {
31
+ x = x.wrapping_add(0x9E37_79B9_7F4A_7C15);
32
+ let mut z = x;
33
+ z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
34
+ z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
35
+ z ^ (z >> 31)
36
+ };
37
+ Rng {
38
+ s: [next(), next(), next(), next()],
39
+ }
40
+ }
41
+
42
+ fn next_u64(&mut self) -> u64 {
43
+ let s = &mut self.s;
44
+ let result = s[1].wrapping_mul(5).rotate_left(7).wrapping_mul(9);
45
+ let t = s[1] << 17;
46
+ s[2] ^= s[0];
47
+ s[3] ^= s[1];
48
+ s[1] ^= s[2];
49
+ s[0] ^= s[3];
50
+ s[2] ^= t;
51
+ s[3] = s[3].rotate_left(45);
52
+ result
53
+ }
54
+
55
+ /// Uniform on [0, 1), with 53 random bits as `random.random()`.
56
+ pub fn random(&mut self) -> f64 {
57
+ (self.next_u64() >> 11) as f64 * (1.0 / (1u64 << 53) as f64)
58
+ }
59
+
60
+ /// `random.expovariate(lambd)`: `-log(1 - random()) / lambd`.
61
+ pub fn expovariate(&mut self, lambd: f64) -> f64 {
62
+ -(1.0 - self.random()).ln() / lambd
63
+ }
64
+
65
+ /// `random.paretovariate(alpha)`: `(1 - random()) ** (-1 / alpha)`.
66
+ pub fn paretovariate(&mut self, alpha: f64) -> f64 {
67
+ (1.0 - self.random()).powf(-1.0 / alpha)
68
+ }
69
+
70
+ /// `random.uniform(a, b)`: `a + (b - a) * random()`.
71
+ pub fn uniform(&mut self, a: f64, b: f64) -> f64 {
72
+ a + (b - a) * self.random()
73
+ }
74
+ }
75
+
76
+ /// Group sizes of the `batch` pattern, as `smolpy.core.batch.BatchSpec`.
77
+ #[derive(Debug, Clone, Copy, PartialEq)]
78
+ pub enum Batch {
79
+ Fixed(u64),
80
+ Geom(f64),
81
+ Pareto(f64),
82
+ }
83
+
84
+ impl Batch {
85
+ /// E[X], with the same zeta approximation as the reference engine.
86
+ pub fn mean(self) -> f64 {
87
+ match self {
88
+ Batch::Fixed(k) => k as f64,
89
+ Batch::Geom(m) => m,
90
+ Batch::Pareto(a) => zeta(a),
91
+ }
92
+ }
93
+
94
+ pub fn draw(self, rng: &mut Rng) -> u64 {
95
+ match self {
96
+ Batch::Fixed(k) => k,
97
+ Batch::Geom(m) => {
98
+ let p = 1.0 / m;
99
+ if p >= 1.0 {
100
+ return 1;
101
+ }
102
+ // inverse transform for the geometric distribution on {1, 2, ...}
103
+ let u = 1.0 - rng.random(); // in (0, 1]
104
+ 1 + (u.ln() / (1.0 - p).ln()) as u64
105
+ }
106
+ Batch::Pareto(a) => (rng.paretovariate(a) as u64).max(1),
107
+ }
108
+ }
109
+ }
110
+
111
+ /// Riemann zeta(s) for s > 1: direct sum plus an Euler-Maclaurin tail, as
112
+ /// `smolpy.core.batch._zeta` (whose sum is exact; this one is compensated).
113
+ pub fn zeta(s: f64) -> f64 {
114
+ let terms = 10_000;
115
+ let n = terms as f64;
116
+ let (mut sum, mut c) = (0.0f64, 0.0f64);
117
+ for k in 1..terms {
118
+ let y = (k as f64).powf(-s) - c;
119
+ let t = sum + y;
120
+ c = (t - sum) - y;
121
+ sum = t;
122
+ }
123
+ let tail = n.powf(1.0 - s) / (s - 1.0) + 0.5 * n.powf(-s) + s * n.powf(-s - 1.0) / 12.0;
124
+ sum + tail
125
+ }
126
+
127
+ #[cfg(test)]
128
+ mod tests {
129
+ use super::*;
130
+
131
+ #[test]
132
+ fn stream_seed_is_a_sha256_prefix() {
133
+ // the value of Model Semantics §6 and tests/test_semantics.py
134
+ assert_eq!(stream_seed(42, "flow:a->b#0"), 5_297_917_812_165_586_412);
135
+ }
136
+
137
+ #[test]
138
+ fn a_seed_gives_the_same_draws() {
139
+ let (mut a, mut b) = (Rng::new(7), Rng::new(7));
140
+ for _ in 0..1000 {
141
+ assert_eq!(a.random().to_bits(), b.random().to_bits());
142
+ }
143
+ assert_ne!(Rng::new(7).random(), Rng::new(8).random());
144
+ }
145
+
146
+ fn mean(n: usize, mut f: impl FnMut() -> f64) -> f64 {
147
+ (0..n).map(|_| f()).sum::<f64>() / n as f64
148
+ }
149
+
150
+ #[test]
151
+ fn distributions_have_their_means() {
152
+ let mut r = Rng::new(1);
153
+ let n = 400_000;
154
+ let u = mean(n, || r.random());
155
+ assert!((u - 0.5).abs() < 0.003, "{u}");
156
+ assert!((0..n).all(|_| (0.0..1.0).contains(&r.random())));
157
+ let e = mean(n, || r.expovariate(1.0 / 20.0));
158
+ assert!((e / 20.0 - 1.0).abs() < 0.01, "{e}");
159
+ let p = mean(n, || r.paretovariate(3.0)); // mean a / (a - 1) = 1.5
160
+ assert!((p / 1.5 - 1.0).abs() < 0.01, "{p}");
161
+ let un = mean(n, || r.uniform(0.3, 1.2));
162
+ assert!((un / 0.75 - 1.0).abs() < 0.005, "{un}");
163
+ for batch in [Batch::Fixed(8), Batch::Geom(4.0), Batch::Pareto(2.5)] {
164
+ let m = mean(n, || batch.draw(&mut r) as f64);
165
+ assert!((m / batch.mean() - 1.0).abs() < 0.02, "{batch:?}: {m}");
166
+ }
167
+ }
168
+
169
+ #[test]
170
+ fn zeta_values() {
171
+ assert!((zeta(2.0) - std::f64::consts::PI.powi(2) / 6.0).abs() < 1e-12);
172
+ assert!((zeta(2.5) - 1.341_487_257_250_917).abs() < 1e-12);
173
+ }
174
+ }