use super::channel::EpochOutput;
use super::lock::{LockEvent, LockState};
use crate::iq::{Cf64, IqError};
use serde::{Deserialize, Serialize};
use std::io::{BufRead, Write};
pub const EPOCH_SCHEMA: &str = "kshana.track-epoch/1";
pub const BINARY_RECORD_BYTES: usize = 184;
pub const BINARY_TAP_BYTES: usize = 16;
pub const EPOCH_FIELDS: &[&str] = &[
"channel",
"epoch",
"sample_index",
"code_epoch_s",
"t_coh_s",
"periods",
"e_i",
"e_q",
"p_i",
"p_q",
"l_i",
"l_q",
"pll_disc_rad",
"fll_disc_hz",
"dll_disc_chips",
"doppler_hz",
"carrier_phase_cycles",
"code_rate_hz",
"code_phase_chips",
"pli",
"phase_lock",
"code_lock",
"cn0_nwpr_dbhz",
"cn0_beaulieu_dbhz",
"cn0_m2m4_dbhz",
"bit_edge",
"bit",
"state",
];
pub fn tap_fields(n: usize) -> Vec<String> {
(0..n)
.flat_map(|k| {
[
format!("x{k}_offset_chips"),
format!("x{k}_i"),
format!("x{k}_q"),
]
})
.collect()
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ChannelInfo {
pub code: String,
pub design: String,
pub design_hash: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ResolvedRun {
pub acq_coherent_periods: usize,
pub acq_doppler_step_hz: f64,
pub t_track_s: f64,
pub pll_bn_hz: Option<f64>,
pub fll_bn_hz: Option<f64>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct EpochHeader {
pub schema: String,
pub fields: Vec<String>,
pub record_bytes: usize,
pub channels: Vec<ChannelInfo>,
pub sample_rate_hz: f64,
pub engine_version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub recording_sha256: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub extra_taps_chips: Vec<f64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub resolved: Vec<ResolvedRun>,
}
impl EpochHeader {
pub fn new(channels: Vec<ChannelInfo>, sample_rate_hz: f64) -> Self {
Self {
schema: EPOCH_SCHEMA.into(),
fields: EPOCH_FIELDS.iter().map(|s| s.to_string()).collect(),
record_bytes: BINARY_RECORD_BYTES,
channels,
sample_rate_hz,
engine_version: env!("CARGO_PKG_VERSION").into(),
recording_sha256: None,
extra_taps_chips: Vec::new(),
resolved: Vec::new(),
}
}
pub fn with_extra_taps(mut self, taps: &[f64]) -> Self {
self.extra_taps_chips = taps.to_vec();
self.fields = EPOCH_FIELDS
.iter()
.map(|s| s.to_string())
.chain(tap_fields(taps.len()))
.collect();
self.record_bytes = BINARY_RECORD_BYTES + BINARY_TAP_BYTES * taps.len();
self
}
pub fn with_resolved(mut self, resolved: Vec<ResolvedRun>) -> Self {
self.resolved = resolved;
self
}
}
pub trait EpochSink {
fn epoch(&mut self, channel: usize, e: &EpochOutput, state: LockState) -> Result<(), IqError>;
fn event(&mut self, _ev: &LockEvent) -> Result<(), IqError> {
Ok(())
}
fn finish(&mut self) -> Result<(), IqError> {
Ok(())
}
}
fn check_taps(e: &EpochOutput, n: usize) -> Result<(), IqError> {
if e.extra.len() == n {
Ok(())
} else {
Err(IqError::Format(format!(
"an epoch carries {} extra taps but the output was set up for {n}: every channel \
of a run must use the same extra_taps_chips",
e.extra.len()
)))
}
}
fn io(e: std::io::Error) -> IqError {
IqError::Io(e.to_string())
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct TapValue {
pub offset_chips: f64,
pub i: f64,
pub q: f64,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct EpochRecord {
pub channel: u32,
pub epoch: u64,
pub sample_index: u64,
pub code_epoch_s: f64,
pub t_coh_s: f64,
pub periods: u32,
pub e_i: f64,
pub e_q: f64,
pub p_i: f64,
pub p_q: f64,
pub l_i: f64,
pub l_q: f64,
pub pll_disc_rad: f64,
pub fll_disc_hz: f64,
pub dll_disc_chips: f64,
pub doppler_hz: f64,
pub carrier_phase_cycles: f64,
pub code_rate_hz: f64,
pub code_phase_chips: f64,
pub pli: f64,
pub phase_lock: bool,
pub code_lock: bool,
pub cn0_nwpr_dbhz: Option<f64>,
pub cn0_beaulieu_dbhz: Option<f64>,
pub cn0_m2m4_dbhz: Option<f64>,
pub bit_edge: Option<u32>,
pub bit: Option<i8>,
pub state: LockState,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub extra: Vec<TapValue>,
}
impl EpochRecord {
pub fn new(channel: usize, e: &EpochOutput, state: LockState) -> Self {
Self {
channel: channel as u32,
epoch: e.epoch,
sample_index: e.sample_index,
code_epoch_s: e.code_epoch_s,
t_coh_s: e.t_coh_s,
periods: e.periods as u32,
e_i: e.early.re,
e_q: e.early.im,
p_i: e.prompt.re,
p_q: e.prompt.im,
l_i: e.late.re,
l_q: e.late.im,
pll_disc_rad: e.disc.pll_rad,
fll_disc_hz: e.disc.fll_hz,
dll_disc_chips: e.disc.dll_chips,
doppler_hz: e.doppler_hz,
carrier_phase_cycles: e.carrier_phase_cycles,
code_rate_hz: e.code_rate_hz,
code_phase_chips: e.code_phase_chips,
pli: e.pli,
phase_lock: e.phase_lock,
code_lock: e.code_lock,
cn0_nwpr_dbhz: e.cn0_nwpr_dbhz,
cn0_beaulieu_dbhz: e.cn0_beaulieu_dbhz,
cn0_m2m4_dbhz: e.cn0_m2m4_dbhz,
bit_edge: e.bit_edge.map(|b| b as u32),
bit: e.bit,
state,
extra: e
.extra
.iter()
.map(|&(offset_chips, v)| TapValue {
offset_chips,
i: v.re,
q: v.im,
})
.collect(),
}
}
pub fn prompt(&self) -> Cf64 {
Cf64::new(self.p_i, self.p_q)
}
fn csv_row(&self) -> String {
let mut row = self.csv_base_row();
for t in &self.extra {
row.push_str(&format!(",{},{},{}", t.offset_chips, t.i, t.q));
}
row
}
fn csv_base_row(&self) -> String {
let o = |v: Option<f64>| v.map(|x| x.to_string()).unwrap_or_default();
format!(
"{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}",
self.channel,
self.epoch,
self.sample_index,
self.code_epoch_s,
self.t_coh_s,
self.periods,
self.e_i,
self.e_q,
self.p_i,
self.p_q,
self.l_i,
self.l_q,
self.pll_disc_rad,
self.fll_disc_hz,
self.dll_disc_chips,
self.doppler_hz,
self.carrier_phase_cycles,
self.code_rate_hz,
self.code_phase_chips,
self.pli,
self.phase_lock,
self.code_lock,
o(self.cn0_nwpr_dbhz),
o(self.cn0_beaulieu_dbhz),
o(self.cn0_m2m4_dbhz),
self.bit_edge.map(|b| b.to_string()).unwrap_or_default(),
self.bit.map(|b| b.to_string()).unwrap_or_default(),
self.state.as_str(),
)
}
pub fn to_bytes(&self) -> [u8; BINARY_RECORD_BYTES] {
let mut b = [0u8; BINARY_RECORD_BYTES];
let mut flags = 0u8;
for (bit, on) in [
self.phase_lock,
self.code_lock,
self.cn0_nwpr_dbhz.is_some(),
self.cn0_beaulieu_dbhz.is_some(),
self.bit_edge.is_some(),
self.bit.is_some(),
self.cn0_m2m4_dbhz.is_some(),
]
.into_iter()
.enumerate()
{
if on {
flags |= 1 << bit;
}
}
b[0..4].copy_from_slice(&self.channel.to_le_bytes());
b[4] = self.state.code();
b[5] = flags;
b[6] = self.bit.unwrap_or(0) as u8;
b[8..16].copy_from_slice(&self.epoch.to_le_bytes());
b[16..24].copy_from_slice(&self.sample_index.to_le_bytes());
b[24..28].copy_from_slice(&self.periods.to_le_bytes());
b[28..32].copy_from_slice(&self.bit_edge.unwrap_or(0).to_le_bytes());
let floats = [
self.code_epoch_s,
self.t_coh_s,
self.e_i,
self.e_q,
self.p_i,
self.p_q,
self.l_i,
self.l_q,
self.pll_disc_rad,
self.fll_disc_hz,
self.dll_disc_chips,
self.doppler_hz,
self.carrier_phase_cycles,
self.code_rate_hz,
self.code_phase_chips,
self.pli,
self.cn0_nwpr_dbhz.unwrap_or(0.0),
self.cn0_beaulieu_dbhz.unwrap_or(0.0),
self.cn0_m2m4_dbhz.unwrap_or(0.0),
];
for (i, v) in floats.iter().enumerate() {
let at = 32 + 8 * i;
b[at..at + 8].copy_from_slice(&v.to_le_bytes());
}
b
}
pub fn to_vec(&self) -> Vec<u8> {
let mut v = self.to_bytes().to_vec();
for t in &self.extra {
v.extend_from_slice(&t.i.to_le_bytes());
v.extend_from_slice(&t.q.to_le_bytes());
}
v
}
pub fn from_slice(b: &[u8], taps: &[f64]) -> Result<Self, IqError> {
let want = BINARY_RECORD_BYTES + BINARY_TAP_BYTES * taps.len();
if b.len() != want {
return Err(IqError::Format(format!(
"a record is {} bytes; expected {want}",
b.len()
)));
}
let head: &[u8; BINARY_RECORD_BYTES] = b[..BINARY_RECORD_BYTES]
.try_into()
.map_err(|_| IqError::Format("short record".into()))?;
let mut r = Self::from_bytes(head)?;
let f = |at: usize| f64::from_le_bytes(b[at..at + 8].try_into().unwrap_or([0; 8]));
r.extra = taps
.iter()
.enumerate()
.map(|(k, &offset_chips)| {
let at = BINARY_RECORD_BYTES + BINARY_TAP_BYTES * k;
TapValue {
offset_chips,
i: f(at),
q: f(at + 8),
}
})
.collect();
Ok(r)
}
pub fn from_bytes(b: &[u8; BINARY_RECORD_BYTES]) -> Result<Self, IqError> {
let u32_at = |at: usize| u32::from_le_bytes(b[at..at + 4].try_into().unwrap_or([0; 4]));
let u64_at = |at: usize| u64::from_le_bytes(b[at..at + 8].try_into().unwrap_or([0; 8]));
let f = |i: usize| {
let at = 32 + 8 * i;
f64::from_le_bytes(b[at..at + 8].try_into().unwrap_or([0; 8]))
};
let flags = b[5];
let has = |bit: u8| flags & (1 << bit) != 0;
let state = LockState::from_code(b[4])
.ok_or_else(|| IqError::Format(format!("unknown lock-state code {}", b[4])))?;
Ok(Self {
channel: u32_at(0),
epoch: u64_at(8),
sample_index: u64_at(16),
periods: u32_at(24),
code_epoch_s: f(0),
t_coh_s: f(1),
e_i: f(2),
e_q: f(3),
p_i: f(4),
p_q: f(5),
l_i: f(6),
l_q: f(7),
pll_disc_rad: f(8),
fll_disc_hz: f(9),
dll_disc_chips: f(10),
doppler_hz: f(11),
carrier_phase_cycles: f(12),
code_rate_hz: f(13),
code_phase_chips: f(14),
pli: f(15),
phase_lock: has(0),
code_lock: has(1),
cn0_nwpr_dbhz: has(2).then(|| f(16)),
cn0_beaulieu_dbhz: has(3).then(|| f(17)),
bit_edge: has(4).then(|| u32_at(28)),
bit: has(5).then_some(b[6] as i8),
cn0_m2m4_dbhz: has(6).then(|| f(18)),
state,
extra: Vec::new(),
})
}
}
pub struct CsvEpochWriter<W: Write> {
w: W,
n_taps: usize,
}
impl<W: Write> CsvEpochWriter<W> {
pub fn new(w: W) -> Result<Self, IqError> {
Self::with_extra_taps(w, 0)
}
pub fn with_extra_taps(mut w: W, n_taps: usize) -> Result<Self, IqError> {
let cols: Vec<String> = EPOCH_FIELDS
.iter()
.map(|s| s.to_string())
.chain(tap_fields(n_taps))
.collect();
writeln!(w, "{}", cols.join(",")).map_err(io)?;
Ok(Self { w, n_taps })
}
}
impl<W: Write> EpochSink for CsvEpochWriter<W> {
fn epoch(&mut self, channel: usize, e: &EpochOutput, state: LockState) -> Result<(), IqError> {
check_taps(e, self.n_taps)?;
writeln!(self.w, "{}", EpochRecord::new(channel, e, state).csv_row()).map_err(io)
}
fn finish(&mut self) -> Result<(), IqError> {
self.w.flush().map_err(io)
}
}
pub struct JsonlEpochWriter<W: Write> {
w: W,
n_taps: usize,
}
impl<W: Write> JsonlEpochWriter<W> {
pub fn new(mut w: W, header: &EpochHeader) -> Result<Self, IqError> {
let line = serde_json::to_string(&serde_json::json!({ "header": header }))
.map_err(|e| IqError::Format(e.to_string()))?;
writeln!(w, "{line}").map_err(io)?;
Ok(Self {
w,
n_taps: header.extra_taps_chips.len(),
})
}
}
impl<W: Write> EpochSink for JsonlEpochWriter<W> {
fn epoch(&mut self, channel: usize, e: &EpochOutput, state: LockState) -> Result<(), IqError> {
check_taps(e, self.n_taps)?;
let line = serde_json::to_string(&EpochRecord::new(channel, e, state))
.map_err(|e| IqError::Format(e.to_string()))?;
writeln!(self.w, "{line}").map_err(io)
}
fn finish(&mut self) -> Result<(), IqError> {
self.w.flush().map_err(io)
}
}
pub struct BinaryEpochWriter<W: Write> {
w: W,
n_taps: usize,
}
impl<W: Write> BinaryEpochWriter<W> {
pub fn new(mut w: W, header: &EpochHeader) -> Result<Self, IqError> {
let line = serde_json::to_string(header).map_err(|e| IqError::Format(e.to_string()))?;
writeln!(w, "{line}").map_err(io)?;
Ok(Self {
w,
n_taps: header.extra_taps_chips.len(),
})
}
}
impl<W: Write> EpochSink for BinaryEpochWriter<W> {
fn epoch(&mut self, channel: usize, e: &EpochOutput, state: LockState) -> Result<(), IqError> {
check_taps(e, self.n_taps)?;
let rec = EpochRecord::new(channel, e, state);
if self.n_taps == 0 {
self.w.write_all(&rec.to_bytes()).map_err(io)
} else {
self.w.write_all(&rec.to_vec()).map_err(io)
}
}
fn finish(&mut self) -> Result<(), IqError> {
self.w.flush().map_err(io)
}
}
pub struct BinaryEpochReader<R: BufRead> {
header: EpochHeader,
r: R,
}
impl<R: BufRead> BinaryEpochReader<R> {
pub fn new(mut r: R) -> Result<Self, IqError> {
let mut line = Vec::new();
r.read_until(b'\n', &mut line).map_err(io)?;
let header: EpochHeader = serde_json::from_slice(&line)
.map_err(|e| IqError::Format(format!("epoch file header: {e}")))?;
if header.schema != EPOCH_SCHEMA {
return Err(IqError::Format(format!(
"epoch file schema {:?}; expected {EPOCH_SCHEMA:?}",
header.schema
)));
}
let want = BINARY_RECORD_BYTES + BINARY_TAP_BYTES * header.extra_taps_chips.len();
if header.record_bytes != want {
return Err(IqError::Format(format!(
"epoch file records are {} bytes; this reader expects {want} \
({BINARY_RECORD_BYTES} plus {BINARY_TAP_BYTES} per extra tap)",
header.record_bytes
)));
}
Ok(Self { header, r })
}
pub fn header(&self) -> &EpochHeader {
&self.header
}
}
impl<R: BufRead> Iterator for BinaryEpochReader<R> {
type Item = Result<EpochRecord, IqError>;
fn next(&mut self) -> Option<Self::Item> {
let size = self.header.record_bytes;
let mut b = vec![0u8; size];
let mut got = 0;
while got < b.len() {
match self.r.read(&mut b[got..]) {
Ok(0) if got == 0 => return None,
Ok(0) => {
return Some(Err(IqError::Format(format!(
"epoch file ends inside a record ({got} of {size} bytes)"
))))
}
Ok(n) => got += n,
Err(e) => return Some(Err(io(e))),
}
}
Some(EpochRecord::from_slice(&b, &self.header.extra_taps_chips))
}
}
pub struct EventsWriter<W: Write> {
w: W,
}
impl<W: Write> EventsWriter<W> {
pub fn new(w: W) -> Self {
Self { w }
}
}
impl<W: Write> EpochSink for EventsWriter<W> {
fn epoch(&mut self, _: usize, _: &EpochOutput, _: LockState) -> Result<(), IqError> {
Ok(())
}
fn event(&mut self, ev: &LockEvent) -> Result<(), IqError> {
let line = serde_json::to_string(ev).map_err(|e| IqError::Format(e.to_string()))?;
writeln!(self.w, "{line}").map_err(io)
}
fn finish(&mut self) -> Result<(), IqError> {
self.w.flush().map_err(io)
}
}
#[derive(Default)]
pub struct Fanout<'a> {
sinks: Vec<&'a mut dyn EpochSink>,
}
impl<'a> Fanout<'a> {
pub fn new() -> Self {
Self { sinks: Vec::new() }
}
pub fn push(&mut self, s: &'a mut dyn EpochSink) {
self.sinks.push(s);
}
}
impl EpochSink for Fanout<'_> {
fn epoch(&mut self, channel: usize, e: &EpochOutput, state: LockState) -> Result<(), IqError> {
self.sinks
.iter_mut()
.try_for_each(|s| s.epoch(channel, e, state))
}
fn event(&mut self, ev: &LockEvent) -> Result<(), IqError> {
self.sinks.iter_mut().try_for_each(|s| s.event(ev))
}
fn finish(&mut self) -> Result<(), IqError> {
self.sinks.iter_mut().try_for_each(|s| s.finish())
}
}
#[derive(Default)]
pub struct CollectSink {
pub channels: Vec<Vec<(EpochOutput, LockState)>>,
pub events: Vec<LockEvent>,
}
impl EpochSink for CollectSink {
fn epoch(&mut self, channel: usize, e: &EpochOutput, state: LockState) -> Result<(), IqError> {
if self.channels.len() <= channel {
self.channels.resize_with(channel + 1, Vec::new);
}
self.channels[channel].push((e.clone(), state));
Ok(())
}
fn event(&mut self, ev: &LockEvent) -> Result<(), IqError> {
self.events.push(ev.clone());
Ok(())
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
struct Welford {
n: u64,
mean: f64,
m2: f64,
}
impl Welford {
fn push(&mut self, x: f64) {
self.n += 1;
let d = x - self.mean;
self.mean += d / self.n as f64;
self.m2 += d * (x - self.mean);
}
fn std(&self) -> f64 {
if self.n < 2 {
0.0
} else {
(self.m2 / (self.n - 1) as f64).sqrt()
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ChannelSummary {
pub epochs: u64,
pub phase_lock_epochs: u64,
pub code_lock_epochs: u64,
pub locked_epochs: u64,
pub transitions: u64,
pub false_locks: u64,
pub reacquisitions: u64,
pub last: Option<EpochRecord>,
pub final_state: Option<LockState>,
pub mean_cn0_dbhz: Option<f64>,
pub phase_jitter_deg: f64,
pub code_jitter_chips: f64,
cn0_sum: f64,
cn0_n: u64,
pll: Welford,
dll: Welford,
}
impl ChannelSummary {
pub fn phase_lock_fraction(&self) -> f64 {
ratio(self.phase_lock_epochs, self.epochs)
}
pub fn code_lock_fraction(&self) -> f64 {
ratio(self.code_lock_epochs, self.epochs)
}
pub fn locked_fraction(&self) -> f64 {
ratio(self.locked_epochs, self.epochs)
}
pub fn locked_at_end(&self) -> bool {
self.last
.as_ref()
.is_some_and(|l| l.phase_lock && l.code_lock)
}
}
fn ratio(a: u64, b: u64) -> f64 {
if b == 0 {
0.0
} else {
a as f64 / b as f64
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Summary {
pub channels: Vec<ChannelSummary>,
steady_from_s: f64,
}
impl Summary {
pub fn new(steady_from_s: f64) -> Self {
Self {
channels: Vec::new(),
steady_from_s,
}
}
}
impl EpochSink for Summary {
fn epoch(&mut self, channel: usize, e: &EpochOutput, state: LockState) -> Result<(), IqError> {
if self.channels.len() <= channel {
self.channels
.resize_with(channel + 1, ChannelSummary::default);
}
let c = &mut self.channels[channel];
c.epochs += 1;
c.phase_lock_epochs += u64::from(e.phase_lock);
c.code_lock_epochs += u64::from(e.code_lock);
c.locked_epochs += u64::from(state == LockState::Locked);
if let Some(cn0) = e.cn0_nwpr_dbhz {
c.cn0_sum += cn0;
c.cn0_n += 1;
c.mean_cn0_dbhz = Some(c.cn0_sum / c.cn0_n as f64);
}
if e.code_epoch_s >= self.steady_from_s {
c.pll.push(e.disc.pll_rad);
c.dll.push(e.disc.dll_chips);
c.phase_jitter_deg = c.pll.std().to_degrees();
c.code_jitter_chips = c.dll.std();
}
c.last = Some(EpochRecord::new(channel, e, state));
c.final_state = Some(state);
Ok(())
}
fn event(&mut self, ev: &LockEvent) -> Result<(), IqError> {
let ch = ev.channel as usize;
if self.channels.len() <= ch {
self.channels.resize_with(ch + 1, ChannelSummary::default);
}
let c = &mut self.channels[ch];
if ev.from != ev.to {
c.transitions += 1;
}
if ev.reason == "false-lock" {
c.false_locks += 1;
}
if ev.reason == "reacquired" {
c.reacquisitions += 1;
}
c.final_state = Some(ev.to);
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EpochFormat {
Csv,
Jsonl,
Binary,
}
impl EpochFormat {
pub fn parse(s: &str) -> Result<Self, String> {
match s.to_ascii_lowercase().as_str() {
"csv" => Ok(Self::Csv),
"jsonl" | "jsonlines" => Ok(Self::Jsonl),
"bin" | "binary" => Ok(Self::Binary),
o => Err(format!("epoch format {o:?}: expected csv, jsonl or bin")),
}
}
pub fn from_path(p: &str) -> Option<Self> {
let l = p.to_ascii_lowercase();
if l.ends_with(".csv") {
Some(Self::Csv)
} else if l.ends_with(".jsonl") {
Some(Self::Jsonl)
} else if l.ends_with(".bin") || l.ends_with(".epochs") {
Some(Self::Binary)
} else {
None
}
}
pub fn writer<'w, W: Write + 'w>(
self,
w: W,
header: &EpochHeader,
) -> Result<Box<dyn EpochSink + 'w>, IqError> {
Ok(match self {
Self::Csv => Box::new(CsvEpochWriter::with_extra_taps(
w,
header.extra_taps_chips.len(),
)?),
Self::Jsonl => Box::new(JsonlEpochWriter::new(w, header)?),
Self::Binary => Box::new(BinaryEpochWriter::new(w, header)?),
})
}
}