use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::document::{validate_header, validate_record, DocError, DocHeader, FrameRecord};
use crate::FrameUid;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EpochRow {
pub simtime: f64,
pub records: Vec<FrameRecord>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FrameSegment {
pub start_simtime: f64,
pub epochs: Vec<EpochRow>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FrameSeries {
pub header: DocHeader,
pub uids: Vec<FrameUid>,
pub segments: Vec<FrameSegment>,
}
impl FrameSeries {
pub fn validate(&self) -> Result<(), DocError> {
validate_header(&self.header)?;
crate::document::validate_uid_table(&self.uids)?;
for (seg_pos, seg) in self.segments.iter().enumerate() {
let Some(first) = seg.epochs.first() else {
return Err(DocError::EmptySegment { segment: seg_pos });
};
if seg.start_simtime.to_bits() != first.simtime.to_bits() {
return Err(DocError::SegmentStartMismatch {
segment: seg_pos,
start: seg.start_simtime,
first_epoch: first.simtime,
});
}
let mut segment_topology: Option<BTreeMap<u32, Option<u32>>> = None;
for row in &seg.epochs {
if !row.simtime.is_finite() {
return Err(DocError::NonFinite(format!(
"segment {seg_pos} epoch simtime = {}",
row.simtime
)));
}
if row.records.len() != self.uids.len() {
return Err(DocError::IncompleteRow {
segment: seg_pos,
simtime: row.simtime,
found: row.records.len(),
expected: self.uids.len(),
});
}
let mut seen = vec![false; self.uids.len()];
for (i, rec) in row.records.iter().enumerate() {
validate_record(rec, i, self.uids.len())?;
let idx = rec.uid_index as usize;
if seen[idx] {
return Err(DocError::DuplicateUid {
index: rec.uid_index,
});
}
seen[idx] = true;
}
let topology = fold_topology(&row.records);
match &segment_topology {
None => segment_topology = Some(topology),
Some(expected) => {
if &topology != expected {
let uid_index = differing_uid(expected, &topology);
return Err(DocError::TopologyMismatch {
segment: seg_pos,
simtime: row.simtime,
uid_index,
});
}
}
}
}
}
Ok(())
}
pub fn to_json_string(&self) -> String {
self.validate().unwrap_or_else(|err| {
panic!("FrameSeries::to_json_string: refusing to serialize an invalid series: {err}")
});
serde_json::to_string(self)
.expect("FrameSeries serialization is infallible after validate()")
}
pub fn from_json_str(json: &str) -> Result<Self, DocError> {
let series: Self = serde_json::from_str(json)?;
series.validate()?;
Ok(series)
}
}
fn fold_topology(records: &[FrameRecord]) -> BTreeMap<u32, Option<u32>> {
records.iter().map(|r| (r.uid_index, r.parent)).collect()
}
fn differing_uid(a: &BTreeMap<u32, Option<u32>>, b: &BTreeMap<u32, Option<u32>>) -> u32 {
for (uid, parent) in a {
if b.get(uid) != Some(parent) {
return *uid;
}
}
for uid in b.keys() {
if !a.contains_key(uid) {
return *uid;
}
}
unreachable!("differing_uid called on identical topologies")
}
#[derive(Debug)]
pub struct SeriesBuilder {
header: DocHeader,
uids: Vec<FrameUid>,
segments: Vec<FrameSegment>,
open: Option<(FrameSegment, BTreeMap<u32, Option<u32>>)>,
}
impl SeriesBuilder {
pub fn new(header: DocHeader, uids: Vec<FrameUid>) -> Self {
Self {
header,
uids,
segments: Vec::new(),
open: None,
}
}
pub fn uids(&self) -> &[FrameUid] {
&self.uids
}
pub fn push_epoch(&mut self, simtime: f64, records: Vec<FrameRecord>) {
assert!(
simtime.is_finite(),
"SeriesBuilder::push_epoch: simtime is non-finite ({simtime}) — fix the \
producing simulation clock"
);
let topology = fold_topology(&records);
let row = EpochRow { simtime, records };
match &mut self.open {
Some((segment, open_topology)) if *open_topology == topology => {
segment.epochs.push(row);
}
_ => {
if let Some((segment, _)) = self.open.take() {
self.segments.push(segment);
}
self.open = Some((
FrameSegment {
start_simtime: simtime,
epochs: vec![row],
},
topology,
));
}
}
}
pub fn finish(mut self) -> FrameSeries {
if let Some((segment, _)) = self.open.take() {
self.segments.push(segment);
}
FrameSeries {
header: self.header,
uids: self.uids,
segments: self.segments,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_fixtures::{
body_record, header, pfix_record, record_bits, root_record, uid_table,
};
fn row() -> Vec<FrameRecord> {
vec![root_record(), pfix_record(), body_record()]
}
fn reparented_row() -> Vec<FrameRecord> {
let mut records = row();
records[2].parent = Some(1);
records
}
#[test]
fn series_builder_single_segment_when_topology_stable() {
let mut builder = SeriesBuilder::new(header(), uid_table());
for i in 0..5 {
builder.push_epoch(f64::from(i), row());
}
let series = builder.finish();
series.validate().expect("stable-topology series validates");
assert_eq!(series.segments.len(), 1);
assert_eq!(series.segments[0].epochs.len(), 5);
assert_eq!(
series.segments[0].start_simtime.to_bits(),
0.0_f64.to_bits()
);
}
#[test]
fn series_builder_opens_new_segment_on_reparent() {
let mut builder = SeriesBuilder::new(header(), uid_table());
builder.push_epoch(0.0, row());
builder.push_epoch(1.0, row());
builder.push_epoch(2.0, reparented_row()); builder.push_epoch(3.0, reparented_row());
let series = builder.finish();
series.validate().expect("segmented series validates");
assert_eq!(series.segments.len(), 2, "reparent must close the segment");
assert_eq!(series.segments[0].epochs.len(), 2);
assert_eq!(series.segments[1].epochs.len(), 2);
assert_eq!(
series.segments[1].start_simtime.to_bits(),
2.0_f64.to_bits()
);
}
#[test]
fn series_round_trips_bit_exact() {
let mut builder = SeriesBuilder::new(header(), uid_table());
builder.push_epoch(0.5, row());
builder.push_epoch(1.5, reparented_row());
let series = builder.finish();
let json = series.to_json_string();
let back = FrameSeries::from_json_str(&json).expect("round trip");
assert_eq!(back.segments.len(), series.segments.len());
for (sa, sb) in series.segments.iter().zip(&back.segments) {
assert_eq!(sa.start_simtime.to_bits(), sb.start_simtime.to_bits());
for (ra, rb) in sa.epochs.iter().zip(&sb.epochs) {
assert_eq!(ra.simtime.to_bits(), rb.simtime.to_bits());
for (a, b) in ra.records.iter().zip(&rb.records) {
assert_eq!(record_bits(a), record_bits(b));
}
}
}
}
#[test]
fn validate_rejects_intra_segment_topology_change() {
let series = FrameSeries {
header: header(),
uids: uid_table(),
segments: vec![FrameSegment {
start_simtime: 0.0,
epochs: vec![
EpochRow {
simtime: 0.0,
records: row(),
},
EpochRow {
simtime: 1.0,
records: reparented_row(),
},
],
}],
};
assert!(matches!(
series.validate(),
Err(DocError::TopologyMismatch {
segment: 0,
uid_index: 2,
..
})
));
}
#[test]
#[should_panic(expected = "simtime is non-finite")]
fn push_epoch_rejects_non_finite_simtime() {
let mut builder = SeriesBuilder::new(header(), uid_table());
builder.push_epoch(f64::NAN, row());
}
#[test]
fn validate_rejects_incomplete_row() {
let mut builder = SeriesBuilder::new(header(), uid_table());
builder.push_epoch(0.0, row());
let mut series = builder.finish();
series.segments[0].epochs[0].records.pop();
assert!(matches!(
series.validate(),
Err(DocError::IncompleteRow {
segment: 0,
found: 2,
expected: 3,
..
})
));
}
#[test]
fn validate_rejects_empty_segment() {
let series = FrameSeries {
header: header(),
uids: uid_table(),
segments: vec![FrameSegment {
start_simtime: 0.0,
epochs: vec![],
}],
};
assert!(matches!(
series.validate(),
Err(DocError::EmptySegment { segment: 0 })
));
}
#[test]
fn validate_rejects_segment_start_mismatch() {
let mut builder = SeriesBuilder::new(header(), uid_table());
builder.push_epoch(1.0, row());
let mut series = builder.finish();
series.segments[0].start_simtime = 0.5;
assert!(matches!(
series.validate(),
Err(DocError::SegmentStartMismatch { segment: 0, .. })
));
}
}