1use crate::engine::rng::SimRng;
10use crate::error::{SimError, SimResult};
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct SIRConfig {
16 pub population: f64,
18 pub initial_infected: f64,
20 pub initial_recovered: f64,
22 pub beta: f64,
24 pub gamma: f64,
26}
27
28impl Default for SIRConfig {
29 fn default() -> Self {
30 Self {
31 population: 1_000_000.0,
32 initial_infected: 100.0,
33 initial_recovered: 0.0,
34 beta: 0.3, gamma: 0.1, }
37 }
38}
39
40impl SIRConfig {
41 #[must_use]
43 pub fn flu() -> Self {
44 Self {
45 population: 1_000_000.0,
46 initial_infected: 10.0,
47 initial_recovered: 0.0,
48 beta: 0.2, gamma: 0.15,
50 }
51 }
52
53 #[must_use]
55 pub fn measles() -> Self {
56 Self {
57 population: 1_000_000.0,
58 initial_infected: 1.0,
59 initial_recovered: 0.0,
60 beta: 1.8, gamma: 0.12,
62 }
63 }
64
65 #[must_use]
69 pub fn r0(&self) -> f64 {
70 if self.gamma == 0.0 {
71 return 0.0;
72 }
73 self.beta / self.gamma
74 }
75
76 #[must_use]
80 pub fn herd_immunity_threshold(&self) -> f64 {
81 let r0 = self.r0();
82 if r0 == 0.0 {
83 return 0.0;
84 }
85 1.0 - 1.0 / r0
86 }
87
88 #[must_use]
90 pub fn initial_susceptible(&self) -> f64 {
91 self.population - self.initial_infected - self.initial_recovered
92 }
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct SEIRConfig {
98 pub sir: SIRConfig,
100 pub initial_exposed: f64,
102 pub sigma: f64,
104}
105
106impl Default for SEIRConfig {
107 fn default() -> Self {
108 Self {
109 sir: SIRConfig::default(),
110 initial_exposed: 50.0,
111 sigma: 0.2, }
113 }
114}
115
116impl SEIRConfig {
117 #[must_use]
119 pub fn initial_susceptible(&self) -> f64 {
120 self.sir.population
121 - self.sir.initial_infected
122 - self.sir.initial_recovered
123 - self.initial_exposed
124 }
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct SIRState {
130 pub susceptible: f64,
132 pub infected: f64,
134 pub recovered: f64,
136 pub time: f64,
138 pub rt: f64,
140}
141
142impl SIRState {
143 #[must_use]
145 pub fn total(&self) -> f64 {
146 self.susceptible + self.infected + self.recovered
147 }
148
149 #[must_use]
151 pub fn infection_rate(&self) -> f64 {
152 let total = self.total();
153 if total == 0.0 {
154 0.0
155 } else {
156 self.infected / total
157 }
158 }
159
160 #[must_use]
162 pub fn susceptible_rate(&self) -> f64 {
163 let total = self.total();
164 if total == 0.0 {
165 0.0
166 } else {
167 self.susceptible / total
168 }
169 }
170
171 #[must_use]
173 pub fn is_over(&self) -> bool {
174 self.infected < 1.0
175 }
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct SEIRState {
181 pub sir: SIRState,
183 pub exposed: f64,
185}
186
187impl SEIRState {
188 #[must_use]
190 pub fn total(&self) -> f64 {
191 self.sir.susceptible + self.exposed + self.sir.infected + self.sir.recovered
192 }
193}
194
195#[derive(Debug, Clone)]
197pub struct SIRScenario {
198 config: SIRConfig,
199 state: SIRState,
200}
201
202impl SIRScenario {
203 #[must_use]
209 #[allow(clippy::many_single_char_names)]
210 pub fn new(config: SIRConfig) -> Self {
211 assert!(config.population > 0.0, "Population must be > 0");
212 assert!(config.gamma > 0.0, "Recovery rate (gamma) must be > 0");
213
214 let s = config.initial_susceptible();
215 let i = config.initial_infected;
216 let r = config.initial_recovered;
217 let n = config.population;
218
219 let state = SIRState {
220 susceptible: s,
221 infected: i,
222 recovered: r,
223 time: 0.0,
224 rt: config.r0() * (s / n),
225 };
226
227 Self { config, state }
228 }
229
230 #[allow(clippy::many_single_char_names)]
236 pub fn step(&mut self, dt: f64) -> SimResult<&SIRState> {
237 let n = self.config.population;
238 let beta = self.config.beta;
239 let gamma = self.config.gamma;
240
241 let s = self.state.susceptible;
243 let i = self.state.infected;
244 let r = self.state.recovered;
245
246 let (k1_s, k1_i, k1_r) = self.derivatives(s, i, r, n, beta, gamma);
248 let (k2_s, k2_i, k2_r) = self.derivatives(
249 s + 0.5 * dt * k1_s,
250 i + 0.5 * dt * k1_i,
251 r + 0.5 * dt * k1_r,
252 n,
253 beta,
254 gamma,
255 );
256 let (k3_s, k3_i, k3_r) = self.derivatives(
257 s + 0.5 * dt * k2_s,
258 i + 0.5 * dt * k2_i,
259 r + 0.5 * dt * k2_r,
260 n,
261 beta,
262 gamma,
263 );
264 let (k4_s, k4_i, k4_r) =
265 self.derivatives(s + dt * k3_s, i + dt * k3_i, r + dt * k3_r, n, beta, gamma);
266
267 let new_s = s + dt / 6.0 * (k1_s + 2.0 * k2_s + 2.0 * k3_s + k4_s);
268 let new_i = i + dt / 6.0 * (k1_i + 2.0 * k2_i + 2.0 * k3_i + k4_i);
269 let new_r = r + dt / 6.0 * (k1_r + 2.0 * k2_r + 2.0 * k3_r + k4_r);
270
271 if new_s < 0.0 || new_i < 0.0 || new_r < 0.0 {
273 return Err(SimError::jidoka(format!(
274 "Non-physical state: S={new_s}, I={new_i}, R={new_r}"
275 )));
276 }
277
278 let total = new_s + new_i + new_r;
280 if (total - n).abs() > 1.0 {
281 return Err(SimError::jidoka(format!(
282 "Population conservation violated: {total} != {n}"
283 )));
284 }
285
286 self.state.susceptible = new_s;
287 self.state.infected = new_i;
288 self.state.recovered = new_r;
289 self.state.time += dt;
290 self.state.rt = self.config.r0() * (new_s / n);
291
292 Ok(&self.state)
293 }
294
295 #[inline]
297 #[allow(clippy::unused_self)]
298 #[allow(clippy::many_single_char_names)]
299 fn derivatives(
300 &self,
301 s: f64,
302 i: f64,
303 _r: f64,
304 n: f64,
305 beta: f64,
306 gamma: f64,
307 ) -> (f64, f64, f64) {
308 let infection = beta * s * i / n;
309 let recovery = gamma * i;
310
311 let ds = -infection;
312 let di = infection - recovery;
313 let dr = recovery;
314
315 (ds, di, dr)
316 }
317
318 pub fn run(&mut self, dt: f64, max_time: f64) -> SimResult<Vec<SIRState>> {
324 let mut trajectory = vec![self.state.clone()];
325
326 while self.state.time < max_time && !self.state.is_over() {
327 self.step(dt)?;
328 trajectory.push(self.state.clone());
329 }
330
331 Ok(trajectory)
332 }
333
334 #[must_use]
336 pub const fn state(&self) -> &SIRState {
337 &self.state
338 }
339
340 #[must_use]
342 pub const fn config(&self) -> &SIRConfig {
343 &self.config
344 }
345
346 #[must_use]
348 pub fn peak_infected(&self) -> f64 {
349 let r0 = self.config.r0();
350 if r0 == 0.0 {
351 return 0.0;
352 }
353 let n = self.config.population;
354 let s0 = self.config.initial_susceptible();
355 let i0 = self.config.initial_infected;
356
357 if s0 <= 0.0 {
358 return i0;
359 }
360
361 let s_peak = n / r0;
363
364 s0 + i0 - s_peak + (n / r0) * (s_peak / s0).ln()
366 }
367
368 #[must_use]
370 pub fn final_size(&self) -> f64 {
371 let r0 = self.config.r0();
372 if r0 == 0.0 {
373 return 0.0;
374 }
375 let n = self.config.population;
376
377 let mut r_inf = 0.8 * n; for _ in 0..50 {
381 let f = r_inf - n * (1.0 - (-r0 * r_inf / n).exp());
382 let df = 1.0 - r0 * (-r0 * r_inf / n).exp();
383 if df.abs() < f64::EPSILON {
384 break;
385 }
386 r_inf -= f / df;
387 }
388
389 r_inf
390 }
391}
392
393#[derive(Debug, Clone)]
395pub struct SEIRScenario {
396 config: SEIRConfig,
397 state: SEIRState,
398}
399
400impl SEIRScenario {
401 #[must_use]
407 #[allow(clippy::many_single_char_names)]
408 pub fn new(config: SEIRConfig) -> Self {
409 assert!(config.sir.population > 0.0, "Population must be > 0");
410 assert!(config.sir.gamma > 0.0, "Recovery rate (gamma) must be > 0");
411
412 let s = config.initial_susceptible();
413 let e = config.initial_exposed;
414 let i = config.sir.initial_infected;
415 let r = config.sir.initial_recovered;
416 let n = config.sir.population;
417
418 let state = SEIRState {
419 sir: SIRState {
420 susceptible: s,
421 infected: i,
422 recovered: r,
423 time: 0.0,
424 rt: config.sir.r0() * (s / n),
425 },
426 exposed: e,
427 };
428
429 Self { config, state }
430 }
431
432 #[allow(clippy::many_single_char_names)]
438 pub fn step(&mut self, dt: f64) -> SimResult<&SEIRState> {
439 let n = self.config.sir.population;
440 let beta = self.config.sir.beta;
441 let gamma = self.config.sir.gamma;
442 let sigma = self.config.sigma;
443
444 let s = self.state.sir.susceptible;
446 let e = self.state.exposed;
447 let i = self.state.sir.infected;
448 let r = self.state.sir.recovered;
449
450 let (k1_s, k1_e, k1_i, k1_r) = self.derivatives(s, e, i, r, n, beta, gamma, sigma);
452 let (k2_s, k2_e, k2_i, k2_r) = self.derivatives(
453 s + 0.5 * dt * k1_s,
454 e + 0.5 * dt * k1_e,
455 i + 0.5 * dt * k1_i,
456 r + 0.5 * dt * k1_r,
457 n,
458 beta,
459 gamma,
460 sigma,
461 );
462 let (k3_s, k3_e, k3_i, k3_r) = self.derivatives(
463 s + 0.5 * dt * k2_s,
464 e + 0.5 * dt * k2_e,
465 i + 0.5 * dt * k2_i,
466 r + 0.5 * dt * k2_r,
467 n,
468 beta,
469 gamma,
470 sigma,
471 );
472 let (k4_s, k4_e, k4_i, k4_r) = self.derivatives(
473 s + dt * k3_s,
474 e + dt * k3_e,
475 i + dt * k3_i,
476 r + dt * k3_r,
477 n,
478 beta,
479 gamma,
480 sigma,
481 );
482
483 let new_s = s + dt / 6.0 * (k1_s + 2.0 * k2_s + 2.0 * k3_s + k4_s);
484 let new_e = e + dt / 6.0 * (k1_e + 2.0 * k2_e + 2.0 * k3_e + k4_e);
485 let new_i = i + dt / 6.0 * (k1_i + 2.0 * k2_i + 2.0 * k3_i + k4_i);
486 let new_r = r + dt / 6.0 * (k1_r + 2.0 * k2_r + 2.0 * k3_r + k4_r);
487
488 if new_s < 0.0 || new_e < 0.0 || new_i < 0.0 || new_r < 0.0 {
490 return Err(SimError::jidoka(format!(
491 "Non-physical state: S={new_s}, E={new_e}, I={new_i}, R={new_r}"
492 )));
493 }
494
495 self.state.sir.susceptible = new_s;
496 self.state.exposed = new_e;
497 self.state.sir.infected = new_i;
498 self.state.sir.recovered = new_r;
499 self.state.sir.time += dt;
500 self.state.sir.rt = self.config.sir.r0() * (new_s / n);
501
502 Ok(&self.state)
503 }
504
505 #[inline]
507 #[allow(clippy::too_many_arguments)]
508 #[allow(clippy::unused_self)]
509 #[allow(clippy::many_single_char_names)]
510 fn derivatives(
511 &self,
512 s: f64,
513 e: f64,
514 i: f64,
515 _r: f64,
516 n: f64,
517 beta: f64,
518 gamma: f64,
519 sigma: f64,
520 ) -> (f64, f64, f64, f64) {
521 let infection = beta * s * i / n;
522 let incubation = sigma * e;
523 let recovery = gamma * i;
524
525 let ds = -infection;
526 let de = infection - incubation;
527 let di = incubation - recovery;
528 let dr = recovery;
529
530 (ds, de, di, dr)
531 }
532
533 pub fn run(&mut self, dt: f64, max_time: f64) -> SimResult<Vec<SEIRState>> {
539 let mut trajectory = vec![self.state.clone()];
540
541 while self.state.sir.time < max_time
542 && (self.state.sir.infected > 1.0 || self.state.exposed > 1.0)
543 {
544 self.step(dt)?;
545 trajectory.push(self.state.clone());
546 }
547
548 Ok(trajectory)
549 }
550
551 #[must_use]
553 pub const fn state(&self) -> &SEIRState {
554 &self.state
555 }
556
557 #[must_use]
559 pub const fn config(&self) -> &SEIRConfig {
560 &self.config
561 }
562}
563
564#[derive(Debug, Clone)]
566pub struct StochasticSIR {
567 config: SIRConfig,
568 state: SIRState,
569}
570
571impl StochasticSIR {
572 #[must_use]
578 #[allow(clippy::many_single_char_names)]
579 pub fn new(config: SIRConfig) -> Self {
580 assert!(config.population > 0.0, "Population must be > 0");
581 assert!(config.gamma > 0.0, "Recovery rate (gamma) must be > 0");
582
583 let s = config.initial_susceptible();
584 let i = config.initial_infected;
585 let r = config.initial_recovered;
586 let n = config.population;
587
588 let state = SIRState {
589 susceptible: s,
590 infected: i,
591 recovered: r,
592 time: 0.0,
593 rt: config.r0() * (s / n),
594 };
595
596 Self { config, state }
597 }
598
599 pub fn step(&mut self, rng: &mut SimRng) -> SimResult<&SIRState> {
605 let n = self.config.population;
606 let beta = self.config.beta;
607 let gamma = self.config.gamma;
608
609 let s = self.state.susceptible;
610 let i = self.state.infected;
611
612 let infection_rate = beta * s * i / n;
614 let recovery_rate = gamma * i;
615 let total_rate = infection_rate + recovery_rate;
616
617 if total_rate <= 0.0 {
618 return Ok(&self.state);
620 }
621
622 let u1: f64 = rng.gen_range_f64(0.0, 1.0);
624 let dt = -u1.ln() / total_rate;
625
626 let u2: f64 = rng.gen_range_f64(0.0, 1.0);
628 if u2 < infection_rate / total_rate {
629 self.state.susceptible -= 1.0;
631 self.state.infected += 1.0;
632 } else {
633 self.state.infected -= 1.0;
635 self.state.recovered += 1.0;
636 }
637
638 self.state.time += dt;
639 self.state.rt = self.config.r0() * (self.state.susceptible / n);
640
641 Ok(&self.state)
642 }
643
644 pub fn run(&mut self, max_time: f64, rng: &mut SimRng) -> SimResult<Vec<SIRState>> {
650 let mut trajectory = vec![self.state.clone()];
651
652 while self.state.time < max_time && self.state.infected >= 1.0 {
653 self.step(rng)?;
654 trajectory.push(self.state.clone());
655 }
656
657 Ok(trajectory)
658 }
659
660 #[must_use]
662 pub const fn state(&self) -> &SIRState {
663 &self.state
664 }
665}
666
667#[cfg(test)]
668mod tests {
669 use super::*;
670
671 #[test]
672 fn test_sir_config_default() {
673 let config = SIRConfig::default();
674
675 assert!((config.population - 1_000_000.0).abs() < f64::EPSILON);
676 assert!(config.initial_infected > 0.0);
677 }
678
679 #[test]
680 fn test_sir_config_r0() {
681 let config = SIRConfig::default();
682 let r0 = config.r0();
683
684 assert!((r0 - 3.0).abs() < 0.01, "R0 = {r0}");
686 }
687
688 #[test]
689 fn test_sir_config_herd_immunity() {
690 let config = SIRConfig::default();
691 let hit = config.herd_immunity_threshold();
692
693 assert!((hit - 0.667).abs() < 0.01, "HIT = {hit}");
695 }
696
697 #[test]
698 fn test_sir_scenario_step() {
699 let config = SIRConfig::default();
700 let mut scenario = SIRScenario::new(config);
701
702 let initial_infected = scenario.state().infected;
703 scenario.step(0.1).unwrap();
704
705 assert!(
707 scenario.state().infected > initial_infected,
708 "Infected should increase when R0 > 1"
709 );
710 }
711
712 #[test]
713 fn test_sir_scenario_conservation() {
714 let config = SIRConfig::default();
715 let mut scenario = SIRScenario::new(config.clone());
716
717 for _ in 0..100 {
719 scenario.step(0.1).unwrap();
720 }
721
722 let total = scenario.state().total();
724 assert!(
725 (total - config.population).abs() < 1.0,
726 "Population not conserved: {total} != {}",
727 config.population
728 );
729 }
730
731 #[test]
732 fn test_sir_scenario_run() {
733 let config = SIRConfig::default();
734 let mut scenario = SIRScenario::new(config);
735
736 let trajectory = scenario.run(0.1, 200.0).unwrap();
737
738 assert!(trajectory.len() > 10);
740
741 let final_state = trajectory.last().unwrap();
743 assert!(final_state.infected < 100.0, "Epidemic should end");
744 }
745
746 #[test]
747 fn test_sir_peak_infected() {
748 let config = SIRConfig::default();
749 let scenario = SIRScenario::new(config.clone());
750
751 let analytical_peak = scenario.peak_infected();
752
753 let mut sim = SIRScenario::new(config);
755 let trajectory = sim.run(0.1, 200.0).unwrap();
756 let numerical_peak = trajectory.iter().map(|s| s.infected).fold(0.0, f64::max);
757
758 let relative_error = (analytical_peak - numerical_peak).abs() / numerical_peak;
760 assert!(
761 relative_error < 0.05,
762 "Analytical peak {analytical_peak} vs numerical {numerical_peak}"
763 );
764 }
765
766 #[test]
767 fn test_sir_final_size() {
768 let config = SIRConfig::default();
769 let scenario = SIRScenario::new(config.clone());
770
771 let analytical_final = scenario.final_size();
772
773 let mut sim = SIRScenario::new(config);
775 let trajectory = sim.run(0.1, 500.0).unwrap();
776 let numerical_final = trajectory.last().unwrap().recovered;
777
778 let relative_error = (analytical_final - numerical_final).abs() / numerical_final;
780 assert!(
781 relative_error < 0.05,
782 "Analytical final size {analytical_final} vs numerical {numerical_final}"
783 );
784 }
785
786 #[test]
787 fn test_seir_scenario_step() {
788 let config = SEIRConfig::default();
789 let mut scenario = SEIRScenario::new(config);
790
791 scenario.step(0.1).unwrap();
792
793 assert!(scenario.state().sir.susceptible >= 0.0);
795 assert!(scenario.state().exposed >= 0.0);
796 assert!(scenario.state().sir.infected >= 0.0);
797 assert!(scenario.state().sir.recovered >= 0.0);
798 }
799
800 #[test]
801 fn test_seir_scenario_run() {
802 let config = SEIRConfig::default();
803 let mut scenario = SEIRScenario::new(config);
804
805 let trajectory = scenario.run(0.1, 200.0).unwrap();
806
807 assert!(trajectory.len() > 10);
808 }
809
810 #[test]
811 fn test_seir_delayed_peak() {
812 let sir_config = SIRConfig::default();
814 let mut sir = SIRScenario::new(sir_config);
815 let sir_trajectory = sir.run(0.1, 200.0).unwrap();
816
817 let seir_config = SEIRConfig::default();
818 let mut seir = SEIRScenario::new(seir_config);
819 let seir_trajectory = seir.run(0.1, 200.0).unwrap();
820
821 let sir_peak_time = sir_trajectory
823 .iter()
824 .max_by(|a, b| a.infected.partial_cmp(&b.infected).unwrap())
825 .map(|s| s.time)
826 .unwrap();
827
828 let seir_peak_time = seir_trajectory
829 .iter()
830 .max_by(|a, b| a.sir.infected.partial_cmp(&b.sir.infected).unwrap())
831 .map(|s| s.sir.time)
832 .unwrap();
833
834 assert!(
836 seir_peak_time > sir_peak_time,
837 "SEIR peak time {seir_peak_time} should be > SIR peak time {sir_peak_time}"
838 );
839 }
840
841 #[test]
842 fn test_stochastic_sir_step() {
843 let config = SIRConfig {
844 population: 1000.0,
845 initial_infected: 10.0,
846 ..Default::default()
847 };
848 let mut scenario = StochasticSIR::new(config);
849 let mut rng = SimRng::new(42);
850
851 scenario.step(&mut rng).unwrap();
852
853 let total = scenario.state().total();
855 assert!((total - 1000.0).abs() < f64::EPSILON);
856 }
857
858 #[test]
859 fn test_stochastic_sir_run() {
860 let config = SIRConfig {
861 population: 1000.0,
862 initial_infected: 10.0,
863 ..Default::default()
864 };
865 let mut scenario = StochasticSIR::new(config);
866 let mut rng = SimRng::new(42);
867
868 let trajectory = scenario.run(500.0, &mut rng).unwrap();
869
870 assert!(trajectory.len() > 100);
872
873 let final_state = trajectory.last().unwrap();
875 assert!(final_state.infected < 1.0);
876 }
877
878 #[test]
879 fn test_sir_flu() {
880 let config = SIRConfig::flu();
881
882 assert!(config.r0() < 2.0);
884 }
885
886 #[test]
887 fn test_sir_measles() {
888 let config = SIRConfig::measles();
889
890 assert!(config.r0() > 10.0);
892
893 assert!(config.herd_immunity_threshold() > 0.9);
895 }
896
897 #[test]
898 fn test_sir_state_is_over() {
899 let state = SIRState {
900 susceptible: 100.0,
901 infected: 0.5,
902 recovered: 899.5,
903 time: 100.0,
904 rt: 0.1,
905 };
906
907 assert!(state.is_over());
908 }
909
910 #[test]
911 fn test_sir_state_infection_rate() {
912 let state = SIRState {
913 susceptible: 500.0,
914 infected: 100.0,
915 recovered: 400.0,
916 time: 10.0,
917 rt: 1.5,
918 };
919
920 assert!((state.infection_rate() - 0.1).abs() < 0.01);
921 assert!((state.susceptible_rate() - 0.5).abs() < 0.01);
922 }
923}
924
925#[cfg(test)]
926mod proptests {
927 use super::*;
928 use proptest::prelude::*;
929
930 proptest! {
931 #[test]
933 fn prop_sir_population_conserved(
934 susceptible in 100.0f64..900.0,
935 infected in 1.0f64..100.0,
936 ) {
937 let recovered = 1000.0 - susceptible - infected;
938 let state = SIRState {
939 susceptible,
940 infected,
941 recovered,
942 time: 0.0,
943 rt: 2.0,
944 };
945
946 let total = state.total();
947 prop_assert!((total - 1000.0).abs() < 0.001, "total={}", total);
948 }
949
950 #[test]
952 fn prop_r0_greater_than_one_grows(
953 beta in 0.3f64..0.8,
954 gamma in 0.05f64..0.15,
955 ) {
956 let r0 = beta / gamma;
957 if r0 > 1.0 {
959 let growth_rate = gamma * (r0 - 1.0);
962 prop_assert!(growth_rate > 0.0);
963 }
964 }
965
966 #[test]
968 fn prop_seir_population_conserved(
969 susceptible in 100.0f64..800.0,
970 exposed in 1.0f64..50.0,
971 infected in 1.0f64..50.0,
972 ) {
973 let recovered = 1000.0 - susceptible - exposed - infected;
974 let sir = SIRState {
975 susceptible,
976 infected,
977 recovered,
978 time: 0.0,
979 rt: 2.0,
980 };
981 let state = SEIRState { sir, exposed };
982
983 let total = state.total();
984 prop_assert!((total - 1000.0).abs() < 0.001, "total={}", total);
985 }
986
987 #[test]
989 fn prop_infection_rate_nonnegative(
990 infected in 0.0f64..500.0,
991 total in 500.0f64..2000.0,
992 ) {
993 let susceptible = total - infected;
994 let state = SIRState {
995 susceptible,
996 infected,
997 recovered: 0.0,
998 time: 0.0,
999 rt: 2.0,
1000 };
1001
1002 let rate = state.infection_rate();
1003 prop_assert!(rate >= 0.0, "rate={}", rate);
1004 prop_assert!(rate <= 1.0, "rate={} should be <= 1", rate);
1005 }
1006
1007 #[test]
1009 fn prop_epidemic_over_threshold(
1010 infected in 0.0f64..10.0,
1011 ) {
1012 let state = SIRState {
1013 susceptible: 100.0,
1014 infected,
1015 recovered: 900.0 - infected,
1016 time: 100.0,
1017 rt: 0.5,
1018 };
1019
1020 if infected < 1.0 {
1022 prop_assert!(state.is_over());
1023 }
1024 }
1025 }
1026}