use serde::{Deserialize, Serialize};
use crate::schema::SensorId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ChannelId(pub u16);
impl ChannelId {
pub fn index(self) -> usize {
self.0 as usize
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[repr(u16)]
pub enum Unit {
Dimensionless = 0,
Kilohertz = 1,
Celsius = 2,
Microjoule = 3,
Microwatt = 4,
Nanosecond = 5,
Microsecond = 6,
Byte = 7,
Count = 8,
Ratio = 9,
Boolean = 10,
Ordinal = 11,
}
impl Unit {
pub fn as_u16(self) -> u16 {
self as u16
}
pub fn from_u16(value: u16) -> Unit {
match value {
1 => Unit::Kilohertz,
2 => Unit::Celsius,
3 => Unit::Microjoule,
4 => Unit::Microwatt,
5 => Unit::Nanosecond,
6 => Unit::Microsecond,
7 => Unit::Byte,
8 => Unit::Count,
9 => Unit::Ratio,
10 => Unit::Boolean,
11 => Unit::Ordinal,
_ => Unit::Dimensionless,
}
}
pub fn symbol(self) -> &'static str {
match self {
Unit::Dimensionless => "",
Unit::Kilohertz => "kHz",
Unit::Celsius => "C",
Unit::Microjoule => "uJ",
Unit::Microwatt => "uW",
Unit::Nanosecond => "ns",
Unit::Microsecond => "us",
Unit::Byte => "B",
Unit::Count => "",
Unit::Ratio => "",
Unit::Boolean => "",
Unit::Ordinal => "",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[repr(u16)]
pub enum Semantics {
Instant = 0,
Cumulative = 1,
Configured = 2,
Ordinal = 3,
}
impl Semantics {
pub fn as_u16(self) -> u16 {
self as u16
}
pub fn from_u16(value: u16) -> Semantics {
match value {
1 => Semantics::Cumulative,
2 => Semantics::Configured,
3 => Semantics::Ordinal,
_ => Semantics::Instant,
}
}
pub fn label(self) -> &'static str {
match self {
Semantics::Instant => "instant",
Semantics::Cumulative => "cumulative",
Semantics::Configured => "configured",
Semantics::Ordinal => "ordinal",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChannelSpec {
pub id: ChannelId,
pub key: String,
pub unit: Unit,
pub semantics: Semantics,
pub sensor: SensorId,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateMatrix {
rows: usize,
cols: usize,
#[serde(with = "crate::nanjson::vec")]
values: Vec<f64>,
}
impl PartialEq for StateMatrix {
fn eq(&self, other: &StateMatrix) -> bool {
self.rows == other.rows
&& self.cols == other.cols
&& self
.values
.iter()
.zip(&other.values)
.all(|(a, b)| crate::relation::same_value(*a, *b))
}
}
impl StateMatrix {
pub fn new(rows: usize, cols: usize) -> StateMatrix {
StateMatrix {
rows,
cols,
values: vec![f64::NAN; rows * cols],
}
}
pub fn rows(&self) -> usize {
self.rows
}
pub fn cols(&self) -> usize {
self.cols
}
pub fn clear(&mut self) {
self.values.fill(f64::NAN);
}
#[inline]
pub fn get(&self, row: usize, col: usize) -> f64 {
self.values[row * self.cols + col]
}
#[inline]
pub fn set(&mut self, row: usize, col: usize, value: f64) {
self.values[row * self.cols + col] = value;
}
pub fn row(&self, row: usize) -> &[f64] {
let start = row * self.cols;
&self.values[start..start + self.cols]
}
pub fn as_slice(&self) -> &[f64] {
&self.values
}
pub fn copy_from_slice(&mut self, values: &[f64]) {
assert_eq!(
values.len(),
self.values.len(),
"state matrix shape mismatch"
);
self.values.copy_from_slice(values);
}
pub fn observed_cells(&self) -> usize {
self.values.iter().filter(|v| !v.is_nan()).count()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn matrices_with_the_same_holes_are_equal() {
let a = StateMatrix::new(2, 2);
let b = StateMatrix::new(2, 2);
assert_eq!(a, b, "unobserved must compare equal to unobserved");
let mut c = StateMatrix::new(2, 2);
c.set(0, 0, 1.0);
assert_ne!(a, c);
}
#[test]
fn a_new_matrix_is_entirely_unobserved() {
let m = StateMatrix::new(3, 4);
assert_eq!(m.rows(), 3);
assert_eq!(m.cols(), 4);
assert_eq!(m.observed_cells(), 0);
assert!(m.get(2, 3).is_nan());
}
#[test]
fn absent_is_distinguishable_from_zero() {
let mut m = StateMatrix::new(2, 2);
m.set(0, 0, 0.0);
assert_eq!(m.get(0, 0), 0.0);
assert!(!m.get(0, 0).is_nan(), "an observed zero is an observation");
assert!(m.get(1, 1).is_nan(), "an unobserved cell is not a zero");
assert_eq!(m.observed_cells(), 1);
}
#[test]
fn clearing_removes_stale_values() {
let mut m = StateMatrix::new(2, 2);
m.set(0, 0, 42.0);
m.clear();
assert_eq!(m.observed_cells(), 0, "a failed sensor must leave a hole");
}
#[test]
fn rows_are_contiguous_state_vectors() {
let mut m = StateMatrix::new(2, 3);
for col in 0..3 {
m.set(1, col, col as f64);
}
assert_eq!(m.row(1), &[0.0, 1.0, 2.0]);
}
#[test]
fn flat_layout_is_row_major() {
let mut m = StateMatrix::new(2, 3);
m.set(0, 0, 1.0);
m.set(1, 0, 2.0);
assert_eq!(m.as_slice()[0], 1.0);
assert_eq!(m.as_slice()[3], 2.0, "row 1 starts at index cols");
}
#[test]
fn round_trips_through_a_flat_slice() {
let mut a = StateMatrix::new(2, 2);
a.set(0, 1, 7.5);
let mut b = StateMatrix::new(2, 2);
b.copy_from_slice(a.as_slice());
assert_eq!(a.get(0, 1), b.get(0, 1));
assert!(b.get(1, 0).is_nan(), "NaN must survive the copy");
}
#[test]
fn wire_codes_round_trip_and_degrade() {
for unit in [
Unit::Kilohertz,
Unit::Celsius,
Unit::Microjoule,
Unit::Count,
] {
assert_eq!(Unit::from_u16(unit.as_u16()), unit);
}
assert_eq!(Unit::from_u16(9999), Unit::Dimensionless);
for s in [
Semantics::Instant,
Semantics::Cumulative,
Semantics::Configured,
] {
assert_eq!(Semantics::from_u16(s.as_u16()), s);
}
assert_eq!(Semantics::from_u16(9999), Semantics::Instant);
}
}