1use core::fmt;
83
84pub const DEFAULT_INTERVAL_SECONDS: f64 = 0.1;
86
87pub const BOUNDARY_TOLERANCE_SCALE: f64 = 1e-12;
97
98pub const MAX_SAMPLE_INDEX: u64 = 1 << f64::MANTISSA_DIGITS;
105
106#[derive(Clone, Copy, Debug, PartialEq)]
108pub struct FixedRateScheduleConfig {
109 pub interval_seconds: f64,
112}
113
114impl Default for FixedRateScheduleConfig {
115 fn default() -> Self {
117 Self {
118 interval_seconds: DEFAULT_INTERVAL_SECONDS,
119 }
120 }
121}
122
123impl FixedRateScheduleConfig {
124 #[must_use]
127 pub fn is_valid(self) -> bool {
128 self.interval_seconds.is_finite() && self.interval_seconds > 0.0
129 }
130}
131
132#[derive(Clone, Copy, Debug, Eq, PartialEq)]
134pub enum FixedRateScheduleConfigError {
135 InvalidInterval,
137 InvalidPhaseOffset,
139}
140
141impl fmt::Display for FixedRateScheduleConfigError {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 f.write_str(match self {
144 Self::InvalidInterval => "sample interval must be finite and positive",
145 Self::InvalidPhaseOffset => {
146 "phase offset must be finite, at least zero and less than the interval"
147 }
148 })
149 }
150}
151
152impl std::error::Error for FixedRateScheduleConfigError {}
153
154#[derive(Clone, Copy, Debug, Eq, PartialEq)]
156pub enum FixedRateScheduleAdvanceError {
157 InvalidElapsed,
161}
162
163impl fmt::Display for FixedRateScheduleAdvanceError {
164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165 f.write_str(match self {
166 Self::InvalidElapsed => {
167 "elapsed time must be finite, not negative, and reach a representable next sample"
168 }
169 })
170 }
171}
172
173impl std::error::Error for FixedRateScheduleAdvanceError {}
174
175#[derive(Clone, Copy, Debug, PartialEq)]
177pub struct FixedRateSample {
178 pub scheduled_simulation_time_seconds: f64,
181 pub offset_within_update_seconds: f64,
184 pub elapsed_since_previous_sample_seconds: f64,
188}
189
190#[derive(Clone, Copy, Debug, PartialEq)]
194pub struct FixedRateSchedule {
195 config: FixedRateScheduleConfig,
196 phase_offset_seconds: f64,
197 simulation_time_seconds: f64,
198 last_sample_time_seconds: f64,
199 next_sample_index: u64,
202}
203
204impl Default for FixedRateSchedule {
205 fn default() -> Self {
207 Self::new(FixedRateScheduleConfig::default(), 0.0)
208 .expect("default fixed-rate schedule is valid")
209 }
210}
211
212impl FixedRateSchedule {
213 pub fn new(
228 config: FixedRateScheduleConfig,
229 phase_offset_seconds: f64,
230 ) -> Result<Self, FixedRateScheduleConfigError> {
231 if !config.is_valid() {
232 return Err(FixedRateScheduleConfigError::InvalidInterval);
233 }
234 if !phase_offset_seconds.is_finite()
235 || phase_offset_seconds < 0.0
236 || phase_offset_seconds >= config.interval_seconds
237 {
238 return Err(FixedRateScheduleConfigError::InvalidPhaseOffset);
239 }
240 let next_sample_index = u64::from(phase_offset_seconds == 0.0);
243 Ok(Self {
244 config,
245 phase_offset_seconds,
246 simulation_time_seconds: 0.0,
247 last_sample_time_seconds: 0.0,
248 next_sample_index,
249 })
250 }
251
252 #[must_use]
254 pub fn phase_offset_seconds(&self) -> f64 {
255 self.phase_offset_seconds
256 }
257
258 #[must_use]
260 pub fn simulation_time_seconds(&self) -> f64 {
261 self.simulation_time_seconds
262 }
263
264 pub fn skip_elapsed(
280 &mut self,
281 elapsed_seconds: f64,
282 ) -> Result<(), FixedRateScheduleAdvanceError> {
283 if !elapsed_seconds.is_finite() || elapsed_seconds < 0.0 {
284 return Err(FixedRateScheduleAdvanceError::InvalidElapsed);
285 }
286 if elapsed_seconds == 0.0 {
287 return Ok(());
288 }
289 let skipped_end_seconds = self.simulation_time_seconds + elapsed_seconds;
290 if !skipped_end_seconds.is_finite() {
291 return Err(FixedRateScheduleAdvanceError::InvalidElapsed);
292 }
293
294 let mut next_sample_index = self.next_sample_index;
295 if due_at_or_before(
296 self.sample_time_seconds(next_sample_index),
297 skipped_end_seconds,
298 ) {
299 let estimate = ((skipped_end_seconds - self.phase_offset_seconds)
305 / self.config.interval_seconds)
306 .floor()
307 + 1.0;
308 #[allow(clippy::cast_precision_loss)]
310 let max_index = MAX_SAMPLE_INDEX as f64;
311 if !estimate.is_finite() || estimate > max_index {
313 return Err(FixedRateScheduleAdvanceError::InvalidElapsed);
314 }
315 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
320 let estimate = estimate as u64;
321 next_sample_index = estimate.max(next_sample_index + 1);
322 if next_sample_index - 1 > self.next_sample_index
323 && !due_at_or_before(
324 self.sample_time_seconds(next_sample_index - 1),
325 skipped_end_seconds,
326 )
327 {
328 next_sample_index -= 1;
329 }
330 if due_at_or_before(
331 self.sample_time_seconds(next_sample_index),
332 skipped_end_seconds,
333 ) {
334 next_sample_index += 1;
335 }
336 }
337 let next_sample_time_seconds = self.sample_time_seconds(next_sample_index);
338 if next_sample_index > MAX_SAMPLE_INDEX
339 || !next_sample_time_seconds.is_finite()
340 || due_at_or_before(next_sample_time_seconds, skipped_end_seconds)
341 {
342 return Err(FixedRateScheduleAdvanceError::InvalidElapsed);
343 }
344
345 self.simulation_time_seconds = skipped_end_seconds;
346 self.last_sample_time_seconds = skipped_end_seconds;
347 self.next_sample_index = next_sample_index;
348 Ok(())
349 }
350
351 pub fn advance(
366 &mut self,
367 elapsed_seconds: f64,
368 mut emit: impl FnMut(FixedRateSample),
369 ) -> Result<(), FixedRateScheduleAdvanceError> {
370 if !elapsed_seconds.is_finite() || elapsed_seconds < 0.0 {
371 return Err(FixedRateScheduleAdvanceError::InvalidElapsed);
372 }
373 let update_start_seconds = self.simulation_time_seconds;
374 let update_end_seconds = update_start_seconds + elapsed_seconds;
375 if !update_end_seconds.is_finite() {
376 return Err(FixedRateScheduleAdvanceError::InvalidElapsed);
377 }
378
379 loop {
380 let scheduled_simulation_time_seconds = self.next_sample_time_seconds();
381 if !due_at_or_before(scheduled_simulation_time_seconds, update_end_seconds) {
382 break;
383 }
384 emit(FixedRateSample {
385 scheduled_simulation_time_seconds,
386 offset_within_update_seconds: scheduled_simulation_time_seconds
387 - update_start_seconds,
388 elapsed_since_previous_sample_seconds: scheduled_simulation_time_seconds
389 - self.last_sample_time_seconds,
390 });
391 self.last_sample_time_seconds = scheduled_simulation_time_seconds;
392 self.next_sample_index += 1;
396 }
397 self.simulation_time_seconds = update_end_seconds;
398 Ok(())
399 }
400
401 #[must_use]
414 pub fn next_sample_time_seconds(&self) -> f64 {
415 self.sample_time_seconds(self.next_sample_index)
416 }
417
418 fn sample_time_seconds(&self, index: u64) -> f64 {
420 #[allow(clippy::cast_precision_loss)]
423 let index = index as f64;
424 self.phase_offset_seconds + index * self.config.interval_seconds
425 }
426}
427
428fn due_at_or_before(scheduled_seconds: f64, update_end_seconds: f64) -> bool {
433 scheduled_seconds <= update_end_seconds
434 || scheduled_seconds - update_end_seconds
435 <= BOUNDARY_TOLERANCE_SCALE
436 * scheduled_seconds
437 .abs()
438 .max(update_end_seconds.abs())
439 .max(1.0)
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445
446 const EPSILON: f64 = 1e-12;
447
448 fn event_times(events: &[FixedRateSample]) -> Vec<f64> {
449 events
450 .iter()
451 .map(|event| event.scheduled_simulation_time_seconds)
452 .collect()
453 }
454
455 fn assert_times_match(actual: &[f64], expected: &[f64]) {
456 assert_eq!(actual.len(), expected.len());
457 for (actual, expected) in actual.iter().zip(expected) {
458 assert!((actual - expected).abs() < EPSILON);
459 }
460 }
461
462 fn collect(schedule: &mut FixedRateSchedule, steps: &[f64]) -> Vec<FixedRateSample> {
464 let mut events = Vec::new();
465 for &step in steps {
466 schedule.advance(step, |event| events.push(event)).unwrap();
467 }
468 events
469 }
470
471 #[test]
474 fn default_schedule_emits_at_ten_hertz_boundaries() {
475 let mut schedule = FixedRateSchedule::default();
476 let mut events = Vec::new();
477
478 schedule.advance(0.099, |event| events.push(event)).unwrap();
479 assert!(events.is_empty());
480
481 schedule.advance(0.001, |event| events.push(event)).unwrap();
482 assert_eq!(events.len(), 1);
483 assert!((events[0].scheduled_simulation_time_seconds - 0.1).abs() < EPSILON);
484 assert!((events[0].offset_within_update_seconds - 0.001).abs() < EPSILON);
485 assert!((events[0].elapsed_since_previous_sample_seconds - 0.1).abs() < EPSILON);
486
487 events.clear();
488 schedule.advance(0.2, |event| events.push(event)).unwrap();
489 assert_times_match(&event_times(&events), &[0.2, 0.3]);
490 }
491
492 #[test]
493 fn coarse_and_partitioned_updates_emit_the_same_schedule() {
494 let mut coarse = FixedRateSchedule::default();
495 let mut partitioned = FixedRateSchedule::default();
496 let mut coarse_events = Vec::new();
497 let mut partitioned_events = Vec::new();
498
499 coarse
500 .advance(0.35, |event| coarse_events.push(event))
501 .unwrap();
502 for _ in 0..7 {
503 partitioned
504 .advance(0.05, |event| partitioned_events.push(event))
505 .unwrap();
506 }
507
508 assert_times_match(
509 &event_times(&coarse_events),
510 &event_times(&partitioned_events),
511 );
512 assert!(
513 (coarse.simulation_time_seconds() - partitioned.simulation_time_seconds()).abs()
514 < EPSILON
515 );
516 }
517
518 #[test]
519 fn phase_offsets_stagger_the_first_and_later_samples() {
520 let mut schedule = FixedRateSchedule::new(FixedRateScheduleConfig::default(), 0.025)
521 .expect("phase within the interval");
522 let mut events = Vec::new();
523
524 schedule.advance(0.024, |event| events.push(event)).unwrap();
525 assert!(events.is_empty());
526 schedule.advance(0.001, |event| events.push(event)).unwrap();
527 assert_times_match(&event_times(&events), &[0.025]);
528 assert!((events[0].elapsed_since_previous_sample_seconds - 0.025).abs() < EPSILON);
529
530 events.clear();
531 schedule.advance(0.1, |event| events.push(event)).unwrap();
532 assert_times_match(&event_times(&events), &[0.125]);
533 }
534
535 #[test]
536 fn invalid_configuration_and_elapsed_time_preserve_schedule_state() {
537 let invalid_interval = FixedRateScheduleConfig {
538 interval_seconds: 0.0,
539 };
540 assert_eq!(
541 FixedRateSchedule::new(invalid_interval, 0.0),
542 Err(FixedRateScheduleConfigError::InvalidInterval)
543 );
544 assert_eq!(
545 FixedRateSchedule::new(FixedRateScheduleConfig::default(), 0.1),
546 Err(FixedRateScheduleConfigError::InvalidPhaseOffset)
547 );
548
549 let mut schedule = FixedRateSchedule::default();
550 schedule.advance(0.04, |_| {}).unwrap();
551 let before = schedule;
552 assert_eq!(
553 schedule.advance(f64::NAN, |_| {}),
554 Err(FixedRateScheduleAdvanceError::InvalidElapsed)
555 );
556 assert_eq!(schedule, before);
557 assert_eq!(
558 schedule.advance(-0.1, |_| {}),
559 Err(FixedRateScheduleAdvanceError::InvalidElapsed)
560 );
561 assert_eq!(schedule, before);
562 assert_eq!(
563 schedule.skip_elapsed(f64::INFINITY),
564 Err(FixedRateScheduleAdvanceError::InvalidElapsed)
565 );
566 assert_eq!(schedule, before);
567 }
568
569 #[test]
570 fn skipping_elapsed_time_keeps_phase_without_replaying_samples() {
571 let mut schedule = FixedRateSchedule::new(
572 FixedRateScheduleConfig {
573 interval_seconds: 0.25,
574 },
575 0.1,
576 )
577 .unwrap();
578 schedule.advance(0.05, |_| {}).unwrap();
579 schedule.skip_elapsed(0.96).unwrap();
580
581 assert!((schedule.simulation_time_seconds - 1.01).abs() < EPSILON);
582 assert!((schedule.last_sample_time_seconds - 1.01).abs() < EPSILON);
583 assert!((schedule.next_sample_time_seconds() - 1.1).abs() < EPSILON);
584
585 let mut events = Vec::new();
586 schedule.advance(0.09, |event| events.push(event)).unwrap();
587 assert_times_match(&event_times(&events), &[1.1]);
588 assert!((events[0].elapsed_since_previous_sample_seconds - 0.09).abs() < EPSILON);
589 }
590
591 #[test]
592 fn skipping_unrepresentable_or_tolerance_due_time_preserves_schedule() {
593 let mut tiny_interval = FixedRateSchedule::new(
594 FixedRateScheduleConfig {
595 interval_seconds: f64::MIN_POSITIVE,
596 },
597 0.0,
598 )
599 .unwrap();
600 let tiny_before = tiny_interval;
601 assert_eq!(
602 tiny_interval.skip_elapsed(1.0),
603 Err(FixedRateScheduleAdvanceError::InvalidElapsed)
604 );
605 assert_eq!(tiny_interval, tiny_before);
606
607 let mut tolerance_due = FixedRateSchedule::new(
608 FixedRateScheduleConfig {
609 interval_seconds: 1e-13,
610 },
611 0.0,
612 )
613 .unwrap();
614 let tolerance_before = tolerance_due;
615 assert_eq!(
616 tolerance_due.skip_elapsed(1.0),
617 Err(FixedRateScheduleAdvanceError::InvalidElapsed)
618 );
619 assert_eq!(tolerance_due, tolerance_before);
620 }
621
622 #[test]
623 fn skipping_a_large_elapsed_interval_uses_a_future_sample_boundary() {
624 let mut schedule = FixedRateSchedule::default();
625 schedule.skip_elapsed(1_000_000_000.0).unwrap();
626 assert!(!due_at_or_before(
627 schedule.next_sample_time_seconds(),
628 schedule.simulation_time_seconds
629 ));
630 assert_eq!(
631 schedule.last_sample_time_seconds,
632 schedule.simulation_time_seconds
633 );
634 }
635
636 #[test]
639 fn every_sample_lands_exactly_on_the_index_formula_over_a_long_run() {
640 let phase = 0.03;
643 let config = FixedRateScheduleConfig::default();
644 let mut schedule = FixedRateSchedule::new(config, phase).unwrap();
645 let events = collect(&mut schedule, &[1.0 / 60.0; 60 * 600]);
646
647 assert!(events.len() > 5000, "the run emitted {}", events.len());
648 for (k, event) in events.iter().enumerate() {
649 let expected = phase + k as f64 * config.interval_seconds;
650 assert_eq!(
651 event.scheduled_simulation_time_seconds.to_bits(),
652 expected.to_bits(),
653 "sample {k}"
654 );
655 }
656 }
657
658 #[test]
659 fn a_sample_on_the_update_boundary_fires_in_that_update() {
660 let mut schedule = FixedRateSchedule::default();
664 let mut per_update = Vec::new();
665 for _ in 0..10 {
666 let mut count = 0;
667 schedule.advance(0.1, |_| count += 1).unwrap();
668 per_update.push(count);
669 }
670 assert!(schedule.simulation_time_seconds() < 1.0);
671 assert_eq!(per_update, [1; 10]);
672 }
673
674 #[test]
675 fn split_and_whole_updates_emit_bit_identical_samples() {
676 let config = FixedRateScheduleConfig {
677 interval_seconds: 0.07,
678 };
679 let mut whole = FixedRateSchedule::new(config, 0.011).unwrap();
680 let mut split = FixedRateSchedule::new(config, 0.011).unwrap();
681
682 let whole_events = collect(&mut whole, &[12.5]);
683 let steps: Vec<f64> = [0.013, 0.2, 0.0, 0.041, 0.5, 0.0005]
685 .iter()
686 .copied()
687 .cycle()
688 .take(1000)
689 .collect();
690 let total: f64 = steps.iter().sum();
691 let split_events = collect(&mut split, &steps);
692
693 let covered = total.min(12.5) - config.interval_seconds;
695 let whole_times: Vec<u64> = whole_events
696 .iter()
697 .filter(|event| event.scheduled_simulation_time_seconds < covered)
698 .map(|event| event.scheduled_simulation_time_seconds.to_bits())
699 .collect();
700 let split_times: Vec<u64> = split_events
701 .iter()
702 .filter(|event| event.scheduled_simulation_time_seconds < covered)
703 .map(|event| event.scheduled_simulation_time_seconds.to_bits())
704 .collect();
705 assert!(whole_times.len() > 100);
706 assert_eq!(whole_times, split_times);
707 }
708
709 #[test]
710 fn two_schedules_fed_the_same_inputs_agree_bit_for_bit() {
711 let config = FixedRateScheduleConfig {
712 interval_seconds: 1.0 / 30.0,
713 };
714 let steps = [0.016, 0.017, 0.2, 0.001, 0.033];
715 let mut a = FixedRateSchedule::new(config, 0.005).unwrap();
716 let mut b = FixedRateSchedule::new(config, 0.005).unwrap();
717 let a_events = collect(&mut a, &steps);
718 let b_events = collect(&mut b, &steps);
719
720 assert!(!a_events.is_empty());
721 let bits = |event: &FixedRateSample| {
722 [
723 event.scheduled_simulation_time_seconds.to_bits(),
724 event.offset_within_update_seconds.to_bits(),
725 event.elapsed_since_previous_sample_seconds.to_bits(),
726 ]
727 };
728 assert_eq!(
729 a_events.iter().map(bits).collect::<Vec<_>>(),
730 b_events.iter().map(bits).collect::<Vec<_>>()
731 );
732 assert_eq!(a, b);
733 }
734
735 #[test]
736 fn samples_are_ordered_and_offsets_lie_within_their_update() {
737 let mut schedule = FixedRateSchedule::new(
738 FixedRateScheduleConfig {
739 interval_seconds: 0.02,
740 },
741 0.007,
742 )
743 .unwrap();
744 let mut previous = f64::NEG_INFINITY;
745 for step in [0.1, 0.013, 0.5, 0.019] {
746 schedule
747 .advance(step, |event| {
748 assert!(event.scheduled_simulation_time_seconds > previous);
749 previous = event.scheduled_simulation_time_seconds;
750 assert!(event.offset_within_update_seconds > 0.0);
751 assert!(event.offset_within_update_seconds <= step + EPSILON);
752 })
753 .unwrap();
754 }
755 }
756
757 #[test]
758 fn skipping_emits_nothing_and_resumes_where_advancing_would() {
759 let config = FixedRateScheduleConfig {
760 interval_seconds: 0.1,
761 };
762 for phase in [0.0, 0.013, 0.05, 0.099] {
763 for skip in [0.05, 0.3, 1.0, 7.77, 1234.5678] {
764 let mut skipped = FixedRateSchedule::new(config, phase).unwrap();
765 let mut advanced = skipped;
766 skipped.advance(0.02, |_| {}).unwrap();
767 advanced.advance(0.02, |_| {}).unwrap();
768
769 skipped.skip_elapsed(skip).unwrap();
770 advanced.advance(skip, |_| {}).unwrap();
771
772 let after = 0.25;
773 let skipped_events = collect(&mut skipped, &[after]);
774 let advanced_events = collect(&mut advanced, &[after]);
775 assert_eq!(
776 event_times(&skipped_events)
777 .iter()
778 .map(|time| time.to_bits())
779 .collect::<Vec<_>>(),
780 event_times(&advanced_events)
781 .iter()
782 .map(|time| time.to_bits())
783 .collect::<Vec<_>>(),
784 "phase {phase}, skip {skip}"
785 );
786 let first = skipped_events[0];
789 assert!(
790 (first.elapsed_since_previous_sample_seconds
791 - first.offset_within_update_seconds)
792 .abs()
793 < EPSILON
794 );
795 }
796 }
797 }
798
799 #[test]
800 fn skipping_emits_nothing_on_the_next_update_for_the_skipped_time() {
801 let mut schedule = FixedRateSchedule::default();
802 schedule.skip_elapsed(5.0).unwrap();
803 assert!(collect(&mut schedule, &[0.0]).is_empty());
804 assert!(collect(&mut schedule, &[0.09]).is_empty());
805 }
806
807 #[test]
808 fn a_rejected_skip_past_the_index_bound_leaves_the_schedule_alone() {
809 let mut schedule = FixedRateSchedule::new(
810 FixedRateScheduleConfig {
811 interval_seconds: 1e-3,
812 },
813 0.0,
814 )
815 .unwrap();
816 schedule.advance(0.0105, |_| {}).unwrap();
817 let before = schedule;
818 assert_eq!(
820 schedule.skip_elapsed(1e14),
821 Err(FixedRateScheduleAdvanceError::InvalidElapsed)
822 );
823 assert_eq!(schedule, before);
824 assert_eq!(
825 schedule.skip_elapsed(-1.0),
826 Err(FixedRateScheduleAdvanceError::InvalidElapsed)
827 );
828 assert_eq!(schedule, before);
829 assert_eq!(
830 schedule.advance(f64::INFINITY, |_| panic!("emitted on a rejected update")),
831 Err(FixedRateScheduleAdvanceError::InvalidElapsed)
832 );
833 assert_eq!(schedule, before);
834 }
835
836 #[test]
837 fn phase_stagger_spreads_agents_across_the_interval() {
838 let config = FixedRateScheduleConfig::default();
839 let agents = 8_u32;
840 let mut schedules: Vec<FixedRateSchedule> = (0..agents)
841 .map(|i| {
842 let phase = f64::from(i) * config.interval_seconds / f64::from(agents);
843 FixedRateSchedule::new(config, phase).unwrap()
844 })
845 .collect();
846
847 for schedule in &mut schedules {
850 schedule.advance(config.interval_seconds, |_| {}).unwrap();
851 }
852 let mut times = Vec::new();
853 for schedule in &mut schedules {
854 let events = collect(schedule, &[config.interval_seconds]);
855 assert_eq!(events.len(), 1, "phase {}", schedule.phase_offset_seconds());
856 times.push(events[0].scheduled_simulation_time_seconds);
857 }
858 times.sort_by(f64::total_cmp);
859 let spacing = config.interval_seconds / f64::from(agents);
860 for pair in times.windows(2) {
861 assert!((pair[1] - pair[0] - spacing).abs() < EPSILON);
862 }
863 }
864
865 #[test]
866 fn errors_display_what_was_wrong() {
867 assert!(
868 FixedRateScheduleConfigError::InvalidInterval
869 .to_string()
870 .contains("interval")
871 );
872 assert!(
873 FixedRateScheduleConfigError::InvalidPhaseOffset
874 .to_string()
875 .contains("phase")
876 );
877 assert!(
878 FixedRateScheduleAdvanceError::InvalidElapsed
879 .to_string()
880 .contains("elapsed")
881 );
882 }
883
884 fn first_emitted(schedule: &FixedRateSchedule, step: f64) -> Option<f64> {
887 let mut copy = *schedule;
888 let mut first = None;
889 copy.advance(step, |event| {
890 first.get_or_insert(event.scheduled_simulation_time_seconds);
891 })
892 .unwrap();
893 first
894 }
895
896 #[test]
902 fn the_next_sample_time_is_the_sample_emitted_next_and_reading_it_takes_nothing() {
903 let config = FixedRateScheduleConfig::default();
904 let reach = config.interval_seconds * 2.0;
906 for phase in [0.0, 0.025, 0.099] {
907 let mut schedule = FixedRateSchedule::new(config, phase).expect("phase within");
908 let first = schedule.next_sample_time_seconds();
909 if phase == 0.0 {
910 assert_eq!(first.to_bits(), config.interval_seconds.to_bits());
912 } else {
913 assert_eq!(first.to_bits(), phase.to_bits());
914 }
915
916 for (round, step) in [0.013, 0.05, 0.0, 0.31, 0.1, 0.007].into_iter().enumerate() {
917 let next = schedule.next_sample_time_seconds();
918 assert_eq!(
919 next.to_bits(),
920 schedule.next_sample_time_seconds().to_bits(),
921 "phase {phase}, round {round}: reading it twice moved it"
922 );
923 assert!(next > schedule.simulation_time_seconds());
924 assert_eq!(
925 Some(next.to_bits()),
926 first_emitted(&schedule, reach).map(f64::to_bits),
927 "phase {phase}, round {round}: not the sample emitted next"
928 );
929 if round % 2 == 0 {
930 schedule.advance(step, |_| {}).unwrap();
931 } else {
932 schedule.skip_elapsed(step).unwrap();
933 }
934 }
935 }
936 }
937}