use std::collections::HashMap;
use std::io::Write;
use crate::error::{Mf4Error, Result};
use crate::export::{array_index_suffixes, element_columns};
use crate::model::SignalValues;
use crate::time_ops::SignalSeries;
const MI_INT8: u32 = 1;
const MI_UINT8: u32 = 2;
const MI_INT16: u32 = 3;
const MI_UINT16: u32 = 4;
const MI_INT32: u32 = 5;
const MI_UINT32: u32 = 6;
const MI_SINGLE: u32 = 7;
const MI_DOUBLE: u32 = 9;
const MI_INT64: u32 = 12;
const MI_UINT64: u32 = 13;
const MI_MATRIX: u32 = 14;
const MI_UTF8: u32 = 16;
const MX_CHAR: u8 = 4;
const MX_DOUBLE: u8 = 6;
const MX_SINGLE: u8 = 7;
const MX_INT8: u8 = 8;
const MX_UINT8: u8 = 9;
const MX_INT16: u8 = 10;
const MX_UINT16: u8 = 11;
const MX_INT32: u8 = 12;
const MX_UINT32: u8 = 13;
const MX_INT64: u8 = 14;
const MX_UINT64: u8 = 15;
macro_rules! le_bytes {
($v:expr) => {
$v.iter().flat_map(|x| x.to_le_bytes()).collect::<Vec<u8>>()
};
}
struct FlattenedMat {
name: String,
class: u8,
data_type: u32,
cols: usize,
data: Vec<u8>,
}
pub fn write_mat<W: Write>(series: &[SignalSeries], out: &mut W) -> Result<()> {
write_header(out)?;
let mut names = UniqueNames::default();
for (group_index, group) in time_groups(series).into_iter().enumerate() {
let timestamps = series[group[0]].timestamps();
write_matrix(
out,
&names.claim(&format!("DGM{group_index}_timestamps")),
MX_DOUBLE,
MI_DOUBLE,
timestamps.len(),
1,
&le_bytes!(timestamps),
)?;
for &index in &group {
let s = &series[index];
let mats = flatten_for_mat(s)?;
for item in mats {
write_matrix(
out,
&names.claim(&format!("DG{group_index}_{}", item.name)),
item.class,
item.data_type,
s.len(),
item.cols,
&item.data,
)?;
if let Some(validity) = s.validity() {
let mask: Vec<u8> = validity.iter().map(|&valid| u8::from(!valid)).collect();
write_matrix(
out,
&names.claim(&format!("DG{group_index}_{}_invalid", item.name)),
MX_UINT8,
MI_UINT8,
mask.len(),
1,
&mask,
)?;
}
}
}
}
Ok(())
}
fn time_groups(series: &[SignalSeries]) -> Vec<Vec<usize>> {
let mut groups: Vec<Vec<usize>> = Vec::new();
for (index, s) in series.iter().enumerate() {
match groups.iter_mut().rev().find(|group| {
let known = series[group[0]].timestamps();
let candidate = s.timestamps();
known.len() == candidate.len()
&& known.first() == candidate.first()
&& known.last() == candidate.last()
&& known == candidate
}) {
Some(group) => group.push(index),
None => groups.push(vec![index]),
}
}
groups
}
fn flatten_for_mat(series: &SignalSeries) -> Result<Vec<FlattenedMat>> {
let refuse = |kind: &str| {
Err(Mf4Error::unsupported(
"MAT export",
format!(
"channel '{}' holds {kind} samples, which a numeric MATLAB matrix cannot \
represent; export it to Parquet, or drop it from the selection",
series.name()
),
))
};
Ok(match series.values() {
SignalValues::U8(v) => vec![FlattenedMat {
name: series.name().to_string(),
cols: 1,
class: MX_UINT8,
data_type: MI_UINT8,
data: v.clone(),
}],
SignalValues::I8(v) => vec![FlattenedMat {
name: series.name().to_string(),
cols: 1,
class: MX_INT8,
data_type: MI_INT8,
data: v.iter().map(|&x| x as u8).collect(),
}],
SignalValues::U16(v) => vec![FlattenedMat {
name: series.name().to_string(),
cols: 1,
class: MX_UINT16,
data_type: MI_UINT16,
data: le_bytes!(v),
}],
SignalValues::I16(v) => vec![FlattenedMat {
name: series.name().to_string(),
cols: 1,
class: MX_INT16,
data_type: MI_INT16,
data: le_bytes!(v),
}],
SignalValues::U32(v) => vec![FlattenedMat {
name: series.name().to_string(),
cols: 1,
class: MX_UINT32,
data_type: MI_UINT32,
data: le_bytes!(v),
}],
SignalValues::I32(v) => vec![FlattenedMat {
name: series.name().to_string(),
cols: 1,
class: MX_INT32,
data_type: MI_INT32,
data: le_bytes!(v),
}],
SignalValues::U64(v) => vec![FlattenedMat {
name: series.name().to_string(),
cols: 1,
class: MX_UINT64,
data_type: MI_UINT64,
data: le_bytes!(v),
}],
SignalValues::I64(v) => vec![FlattenedMat {
name: series.name().to_string(),
cols: 1,
class: MX_INT64,
data_type: MI_INT64,
data: le_bytes!(v),
}],
SignalValues::F32(v) => vec![FlattenedMat {
name: series.name().to_string(),
cols: 1,
class: MX_SINGLE,
data_type: MI_SINGLE,
data: le_bytes!(v),
}],
SignalValues::F64(v) => vec![FlattenedMat {
name: series.name().to_string(),
cols: 1,
class: MX_DOUBLE,
data_type: MI_DOUBLE,
data: le_bytes!(v),
}],
SignalValues::Complex { re, im } => vec![
FlattenedMat {
name: format!("{}.re", series.name()),
cols: 1,
class: MX_DOUBLE,
data_type: MI_DOUBLE,
data: le_bytes!(re),
},
FlattenedMat {
name: format!("{}.im", series.name()),
cols: 1,
class: MX_DOUBLE,
data_type: MI_DOUBLE,
data: le_bytes!(im),
},
],
SignalValues::CanopenDate(v) => {
let nanos: Vec<i64> = v.iter().map(|d| d.to_unix_nanos()).collect();
vec![FlattenedMat {
name: series.name().to_string(),
cols: 1,
class: MX_INT64,
data_type: MI_INT64,
data: le_bytes!(nanos),
}]
}
SignalValues::CanopenTime(v) => {
let nanos: Vec<i64> = v.iter().map(|t| t.to_unix_nanos()).collect();
vec![FlattenedMat {
name: series.name().to_string(),
cols: 1,
class: MX_INT64,
data_type: MI_INT64,
data: le_bytes!(nanos),
}]
}
SignalValues::Array {
values,
elements_per_sample,
} => {
let eps = *elements_per_sample;
let suffixes = array_index_suffixes(series.channel.array_shape.as_deref(), eps);
let mut mats = Vec::with_capacity(eps);
for (elem_vals, suffix) in element_columns(values, eps).into_iter().zip(suffixes) {
mats.push(FlattenedMat {
name: format!("{}{suffix}", series.name()),
cols: 1,
class: MX_DOUBLE,
data_type: MI_DOUBLE,
data: le_bytes!(elem_vals),
});
}
mats
}
SignalValues::ArrayVarLen { .. } => {
return Err(Mf4Error::unsupported(
"MAT export",
format!(
"channel '{}' holds variable-length array samples, which have no fixed column shape and cannot be exported to a tabular format",
series.name()
),
));
}
SignalValues::Str(v) => {
let rows: Vec<Vec<char>> = v.iter().map(|t| t.chars().collect()).collect();
let width = rows.iter().map(Vec::len).max().unwrap_or(0);
let mut data = Vec::with_capacity(rows.len() * width);
let mut buf = [0u8; 4];
for c in 0..width {
for row in &rows {
let ch = row.get(c).copied().unwrap_or(' ');
data.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
}
}
vec![FlattenedMat {
name: series.name().to_string(),
class: MX_CHAR,
data_type: MI_UTF8,
cols: width,
data,
}]
}
SignalValues::Bytes { data: bytes, width } => {
let n = series.len();
let mut data = Vec::with_capacity(n * width);
for c in 0..*width {
for r in 0..n {
data.push(bytes.get(r * width + c).copied().unwrap_or(0));
}
}
vec![FlattenedMat {
name: series.name().to_string(),
class: MX_UINT8,
data_type: MI_UINT8,
cols: *width,
data,
}]
}
SignalValues::VarBytes { .. } => return refuse("variable-length byte"),
})
}
fn write_header<W: Write>(out: &mut W) -> Result<()> {
let mut header = [b' '; 128];
let text = format!(
"MATLAB 5.0 MAT-file, Platform: {}, Created by: falcon_mdf {}",
std::env::consts::OS,
env!("CARGO_PKG_VERSION")
);
let text = text.as_bytes();
let len = text.len().min(116);
header[..len].copy_from_slice(&text[..len]);
header[124..126].copy_from_slice(&0x0100u16.to_le_bytes());
header[126] = b'I';
header[127] = b'M';
out.write_all(&header)?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn write_matrix<W: Write>(
out: &mut W,
name: &str,
class: u8,
data_type: u32,
rows: usize,
cols: usize,
data: &[u8],
) -> Result<()> {
let rows = i32::try_from(rows).map_err(|_| {
Mf4Error::write_error(format!(
"channel '{name}' has more samples than a MAT-file dimension can hold"
))
})?;
let cols = i32::try_from(cols).map_err(|_| {
Mf4Error::write_error(format!(
"channel '{name}' is wider than a MAT-file dimension can hold"
))
})?;
let mut flags = Vec::with_capacity(8);
flags.extend_from_slice(&(class as u32).to_le_bytes());
flags.extend_from_slice(&0u32.to_le_bytes());
let mut dimensions = Vec::with_capacity(8);
dimensions.extend_from_slice(&rows.to_le_bytes());
dimensions.extend_from_slice(&cols.to_le_bytes());
let mut body = Vec::new();
push_element(&mut body, MI_UINT32, &flags);
push_element(&mut body, MI_INT32, &dimensions);
push_element(&mut body, MI_INT8, name.as_bytes());
push_element(&mut body, data_type, data);
out.write_all(&MI_MATRIX.to_le_bytes())?;
out.write_all(&(body.len() as u32).to_le_bytes())?;
out.write_all(&body)?;
Ok(())
}
fn push_element(buffer: &mut Vec<u8>, data_type: u32, data: &[u8]) {
buffer.extend_from_slice(&data_type.to_le_bytes());
buffer.extend_from_slice(&(data.len() as u32).to_le_bytes());
buffer.extend_from_slice(data);
let padding = (8 - data.len() % 8) % 8;
buffer.extend(std::iter::repeat_n(0u8, padding));
}
#[derive(Default)]
struct UniqueNames {
seen: HashMap<String, usize>,
}
impl UniqueNames {
fn claim(&mut self, name: &str) -> String {
let base = matlab_compatible(name);
match self.seen.get_mut(&base) {
None => {
self.seen.insert(base.clone(), 0);
base
}
Some(count) => {
*count += 1;
format!("{base}_{count}")
}
}
}
}
fn matlab_compatible(name: &str) -> String {
let mut out: String = name
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '_' {
c
} else {
'_'
}
})
.collect();
if !out.chars().next().is_some_and(|c| c.is_ascii_alphabetic()) {
out.insert_str(0, "M_");
}
out.truncate(60);
out
}