use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RevYear {
Y1991,
Y1999,
Y2013,
}
impl fmt::Display for RevYear {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RevYear::Y1991 => write!(f, "1991"),
RevYear::Y1999 => write!(f, "1999"),
RevYear::Y2013 => write!(f, "2013"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DataFormat {
Ascii,
Binary16,
Binary32,
Float32,
}
impl fmt::Display for DataFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DataFormat::Ascii => write!(f, "ASCII"),
DataFormat::Binary16 => write!(f, "BINARY"),
DataFormat::Binary32 => write!(f, "BINARY32"),
DataFormat::Float32 => write!(f, "FLOAT32"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScalingFlag {
Primary,
Secondary,
}
#[derive(Debug, Clone)]
pub struct AnalogChannel {
pub index: u32,
pub name: String,
pub phase: String,
pub circuit_component: String,
pub units: String,
pub multiplier: f64,
pub offset: f64,
pub skew: f64,
pub min_value: f64,
pub max_value: f64,
pub primary_ratio: f64,
pub secondary_ratio: f64,
pub scaling: ScalingFlag,
}
impl AnalogChannel {
pub fn to_raw(&self, value: f64) -> f64 {
if self.multiplier == 0.0 {
0.0
} else {
(value - self.offset) / self.multiplier
}
}
}
#[derive(Debug, Clone)]
pub struct DigitalChannel {
pub index: u32,
pub name: String,
pub phase: String,
pub circuit_component: String,
pub normal_state: u8,
}
#[derive(Debug, Clone, Copy)]
pub struct SampleRate {
pub rate_hz: f64,
pub last_sample: u32,
}
#[derive(Debug, Clone)]
pub struct Sample {
pub number: u32,
pub timestamp_us: f64,
pub analog: Vec<f64>,
pub digital: Vec<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ComtradeTimestamp {
pub day: u32,
pub month: u32,
pub year: u32,
pub hour: u32,
pub minute: u32,
pub second: f64,
}
impl ComtradeTimestamp {
pub fn seconds_since_midnight(&self) -> f64 {
self.hour as f64 * 3600.0 + self.minute as f64 * 60.0 + self.second
}
pub fn fmt_us(&self) -> String {
let whole_sec = self.second as u32;
let frac = self.second - whole_sec as f64;
format!(
"{:02}/{:02}/{:04},{:02}:{:02}:{:02}.{:06}",
self.day,
self.month,
self.year,
self.hour,
self.minute,
whole_sec,
(frac * 1_000_000.0).round() as u64,
)
}
pub fn fmt_ns(&self) -> String {
let whole_sec = self.second as u32;
let frac = self.second - whole_sec as f64;
format!(
"{:02}/{:02}/{:04},{:02}:{:02}:{:02}.{:09}",
self.day,
self.month,
self.year,
self.hour,
self.minute,
whole_sec,
(frac * 1_000_000_000.0).round() as u64,
)
}
}
impl fmt::Display for ComtradeTimestamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.fmt_us())
}
}
#[derive(Debug, Clone)]
pub struct ComtradeRecord {
pub station_name: String,
pub rec_dev_id: String,
pub rev_year: RevYear,
pub frequency: f64,
pub start_time: ComtradeTimestamp,
pub trigger_time: ComtradeTimestamp,
pub analog_channels: Vec<AnalogChannel>,
pub digital_channels: Vec<DigitalChannel>,
pub sample_rates: Vec<SampleRate>,
pub data_format: DataFormat,
pub time_mult: f64,
pub samples: Vec<Sample>,
pub header_text: Option<String>,
pub info: Option<HashMap<String, String>>,
}
impl ComtradeRecord {
pub fn n_analog(&self) -> usize {
self.analog_channels.len()
}
pub fn n_digital(&self) -> usize {
self.digital_channels.len()
}
pub fn n_samples(&self) -> usize {
self.samples.len()
}
pub fn duration_s(&self) -> f64 {
if self.samples.is_empty() {
return 0.0;
}
let last = &self.samples[self.samples.len() - 1];
let first = &self.samples[0];
(last.timestamp_us - first.timestamp_us) * self.time_mult * 1e-6
}
pub fn timestamps_s(&self) -> Vec<f64> {
if self.samples.is_empty() {
return vec![];
}
let t0 = self.samples[0].timestamp_us;
self.samples
.iter()
.map(|s| (s.timestamp_us - t0) * self.time_mult * 1e-6)
.collect()
}
pub fn analog_data(&self, channel_idx: usize) -> Vec<f64> {
self.samples.iter().map(|s| s.analog[channel_idx]).collect()
}
pub fn analog_data_by_name(&self, name: &str) -> Option<Vec<f64>> {
let idx = self.analog_channels.iter().position(|c| c.name == name)?;
Some(self.analog_data(idx))
}
pub fn digital_data(&self, channel_idx: usize) -> Vec<bool> {
self.samples
.iter()
.map(|s| s.digital[channel_idx])
.collect()
}
pub fn digital_transition_time_s(&self, channel_name: &str) -> Option<f64> {
let ch_idx = self
.digital_channels
.iter()
.position(|c| c.name == channel_name)?;
let normal = self.digital_channels[ch_idx].normal_state != 0;
let t0 = self.samples.first()?.timestamp_us;
for s in &self.samples {
if s.digital[ch_idx] != normal {
return Some((s.timestamp_us - t0) * self.time_mult * 1e-6);
}
}
None
}
pub fn validate(&self) -> Result<(), String> {
let na = self.analog_channels.len();
let nd = self.digital_channels.len();
for (i, s) in self.samples.iter().enumerate() {
if s.analog.len() != na {
return Err(format!(
"sample {}: expected {} analog values, got {}",
s.number,
na,
s.analog.len()
));
}
if s.digital.len() != nd {
return Err(format!(
"sample {}: expected {} digital values, got {}",
s.number,
nd,
s.digital.len()
));
}
if i > 0 && s.timestamp_us < self.samples[i - 1].timestamp_us {
return Err(format!(
"sample {}: timestamp {} < previous {}",
s.number,
s.timestamp_us,
self.samples[i - 1].timestamp_us
));
}
}
Ok(())
}
}