Rewrite the driver in Rust; move the testbench tools out
The repository is now only the HPCS 6500 driver: a Rust library (serial transport, every protocol command, block parsing, USBPcap captures, a simulated instrument) and the hpcs6500 CLI with the flags of the former hpcs6500.py, plus dump/capture for reverse engineering. The lamp station, lamp export, TM-30, label printer and LED calibration move to B4L/lamp-testbench. Golden tests pin the Rust output to the Python driver's: parsed values bit-exact, report text, CSV and number formatting. The Python driver is at tag python-final.
This commit is contained in:
1 parent
3ac7cd243b
commit
54242da04b
79 files changed
+23340
-3246
No files matched your search
+267
@@ -0,0 +1,267 @@
|
||||
//! Number formatting that matches Python, so files written by the Rust tools
|
||||
//! are byte-identical to the ones the Python tools wrote (CSV exports, lamp
|
||||
//! bundles): `repr(float)`, `format(x, ".6g")`, `format(x, ".2f")` and
|
||||
//! `round(x, n)`.
|
||||
//!
|
||||
//! Python rounds the exact binary value half to even (David Gay's dtoa);
|
||||
//! Rust's shortest formatting breaks ties differently (`93.72439575195313`
|
||||
//! vs Python's `93.72439575195312`), so the digits are derived here from the
|
||||
//! exact decimal expansion.
|
||||
|
||||
/// Python `repr(x)` / `str(x)` of a float: shortest round-trip digits,
|
||||
/// exponent form below 1e-4 and from 1e16 (`1e-05`, `1e+16`), always a
|
||||
/// `.0` on whole numbers.
|
||||
pub fn repr(x: f64) -> String {
|
||||
if let Some(s) = special(x) {
|
||||
return s;
|
||||
}
|
||||
if x == 0.0 {
|
||||
return if x.is_sign_negative() {
|
||||
"-0.0".into()
|
||||
} else {
|
||||
"0.0".into()
|
||||
};
|
||||
}
|
||||
let (neg, digits, exp) = exact(x);
|
||||
let (mut d, mut e) = (digits.clone(), exp);
|
||||
for p in 1..=17 {
|
||||
let (rd, re) = round_sig(&digits, exp, p);
|
||||
if to_f64(&rd, re) == x.abs() {
|
||||
(d, e) = (rd, re);
|
||||
break;
|
||||
}
|
||||
}
|
||||
trim(&mut d);
|
||||
let mut out = String::with_capacity(24);
|
||||
if neg {
|
||||
out.push('-');
|
||||
}
|
||||
if (-4..16).contains(&e) {
|
||||
place(&mut out, &d, e, 0);
|
||||
if !out.contains('.') {
|
||||
out.push_str(".0");
|
||||
}
|
||||
} else {
|
||||
sci(&mut out, &d, e);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Python `format(x, f".{prec}g")`.
|
||||
pub fn g(x: f64, prec: usize) -> String {
|
||||
if let Some(s) = special(x) {
|
||||
return s;
|
||||
}
|
||||
let p = prec.max(1);
|
||||
if x == 0.0 {
|
||||
return if x.is_sign_negative() {
|
||||
"-0".into()
|
||||
} else {
|
||||
"0".into()
|
||||
};
|
||||
}
|
||||
let (neg, digits, exp) = exact(x);
|
||||
let (mut d, e) = round_sig(&digits, exp, p);
|
||||
trim(&mut d);
|
||||
let mut out = String::new();
|
||||
if neg {
|
||||
out.push('-');
|
||||
}
|
||||
if e >= -4 && e < p as i32 {
|
||||
place(&mut out, &d, e, 0);
|
||||
} else {
|
||||
sci(&mut out, &d, e);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Python `format(x, f".{prec}f")`.
|
||||
pub fn fixed(x: f64, prec: usize) -> String {
|
||||
if let Some(s) = special(x) {
|
||||
return s;
|
||||
}
|
||||
let mut out = String::new();
|
||||
if x.is_sign_negative() {
|
||||
out.push('-');
|
||||
}
|
||||
if x == 0.0 {
|
||||
place(&mut out, b"0", 0, prec);
|
||||
return out;
|
||||
}
|
||||
let (_, digits, exp) = exact(x);
|
||||
// significant digits kept when rounding at the prec-th decimal
|
||||
let keep = exp + 1 + prec as i32;
|
||||
let (d, e) = if keep > 0 {
|
||||
round_sig(&digits, exp, keep as usize)
|
||||
} else if keep == 0 && round_up_from(&digits, 0, b'0') {
|
||||
(b"1".to_vec(), -(prec as i32))
|
||||
} else {
|
||||
(b"0".to_vec(), 0)
|
||||
};
|
||||
place(&mut out, &d, e, prec);
|
||||
out
|
||||
}
|
||||
|
||||
/// Python `round(x, ndigits)` (half to even on the exact value).
|
||||
pub fn round(x: f64, ndigits: usize) -> f64 {
|
||||
if !x.is_finite() {
|
||||
return x;
|
||||
}
|
||||
fixed(x, ndigits).parse().unwrap_or(x)
|
||||
}
|
||||
|
||||
fn special(x: f64) -> Option<String> {
|
||||
if x.is_nan() {
|
||||
Some("nan".into())
|
||||
} else if x.is_infinite() {
|
||||
Some(if x > 0.0 { "inf".into() } else { "-inf".into() })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Exact decimal expansion of a finite non-zero x: (negative, significant
|
||||
/// digits without trailing zeros, decimal exponent of the first digit).
|
||||
fn exact(x: f64) -> (bool, Vec<u8>, i32) {
|
||||
// every f64 has at most 767 significant decimal digits; Rust prints them exactly
|
||||
let s = format!("{:.800e}", x.abs());
|
||||
let (mant, e) = s.split_once('e').expect("exponent");
|
||||
let mut d: Vec<u8> = mant.bytes().filter(u8::is_ascii_digit).collect();
|
||||
trim(&mut d);
|
||||
(x.is_sign_negative(), d, e.parse().expect("exponent"))
|
||||
}
|
||||
|
||||
fn trim(d: &mut Vec<u8>) {
|
||||
while d.len() > 1 && d.last() == Some(&b'0') {
|
||||
d.pop();
|
||||
}
|
||||
}
|
||||
|
||||
/// Does the tail starting at `from` round up: more than half, or exactly
|
||||
/// half after an odd last kept digit?
|
||||
fn round_up_from(d: &[u8], from: usize, last_kept: u8) -> bool {
|
||||
let Some(&first) = d.get(from) else {
|
||||
return false;
|
||||
};
|
||||
if first != b'5' {
|
||||
return first > b'5';
|
||||
}
|
||||
if d[from + 1..].iter().any(|&c| c != b'0') {
|
||||
return true;
|
||||
}
|
||||
(last_kept - b'0') % 2 == 1
|
||||
}
|
||||
|
||||
/// Round to `n` significant digits, half to even.
|
||||
fn round_sig(d: &[u8], exp: i32, n: usize) -> (Vec<u8>, i32) {
|
||||
if d.len() <= n {
|
||||
return (d.to_vec(), exp);
|
||||
}
|
||||
let mut k = d[..n].to_vec();
|
||||
if round_up_from(d, n, k[n - 1]) {
|
||||
let mut i = n;
|
||||
loop {
|
||||
if i == 0 {
|
||||
// 99.9 -> 100: one more digit in front, one fewer at the end
|
||||
k.insert(0, b'1');
|
||||
k.pop();
|
||||
return (k, exp + 1);
|
||||
}
|
||||
i -= 1;
|
||||
if k[i] == b'9' {
|
||||
k[i] = b'0';
|
||||
} else {
|
||||
k[i] += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
(k, exp)
|
||||
}
|
||||
|
||||
fn to_f64(d: &[u8], e: i32) -> f64 {
|
||||
let s = format!(
|
||||
"{}e{}",
|
||||
std::str::from_utf8(d).expect("digits"),
|
||||
e - d.len() as i32 + 1
|
||||
);
|
||||
s.parse().expect("number")
|
||||
}
|
||||
|
||||
/// Positional notation of digits `d` (first digit at 10^e) with at least
|
||||
/// `min_decimals` decimals.
|
||||
fn place(out: &mut String, d: &[u8], e: i32, min_decimals: usize) {
|
||||
// digit at position 10^i
|
||||
let digit = |i: i32| -> char {
|
||||
let idx = e - i;
|
||||
if idx >= 0 && (idx as usize) < d.len() {
|
||||
d[idx as usize] as char
|
||||
} else {
|
||||
'0'
|
||||
}
|
||||
};
|
||||
for i in (0..=e.max(0)).rev() {
|
||||
out.push(digit(i));
|
||||
}
|
||||
let lowest = e - d.len() as i32 + 1;
|
||||
let decimals = (-lowest).max(min_decimals as i32);
|
||||
if decimals > 0 {
|
||||
out.push('.');
|
||||
for i in 1..=decimals {
|
||||
out.push(digit(-i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sci(out: &mut String, d: &[u8], e: i32) {
|
||||
out.push(d[0] as char);
|
||||
if d.len() > 1 {
|
||||
out.push('.');
|
||||
out.push_str(std::str::from_utf8(&d[1..]).expect("digits"));
|
||||
}
|
||||
out.push('e');
|
||||
out.push(if e < 0 { '-' } else { '+' });
|
||||
let a = e.unsigned_abs();
|
||||
if a < 10 {
|
||||
out.push('0');
|
||||
}
|
||||
out.push_str(&a.to_string());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::excessive_precision)]
|
||||
fn examples() {
|
||||
assert_eq!(repr(1.0), "1.0");
|
||||
assert_eq!(repr(100.0), "100.0");
|
||||
assert_eq!(repr(0.1 + 0.2), "0.30000000000000004");
|
||||
assert_eq!(repr(1e-5), "1e-05");
|
||||
assert_eq!(repr(0.0001), "0.0001");
|
||||
assert_eq!(repr(1e16), "1e+16");
|
||||
assert_eq!(repr(1234567890123456.0), "1234567890123456.0");
|
||||
assert_eq!(repr(93.724395751953125), "93.72439575195312");
|
||||
assert_eq!(repr(-617.35406494140625), "-617.3540649414062");
|
||||
assert_eq!(g(0.00191776, 6), "0.00191776");
|
||||
assert_eq!(g(1.5e-5, 6), "1.5e-05");
|
||||
assert_eq!(g(123456.5, 6), "123456");
|
||||
assert_eq!(g(1234567.0, 6), "1.23457e+06");
|
||||
assert_eq!(g(999999.5, 6), "1e+06");
|
||||
assert_eq!(g(100.0, 6), "100");
|
||||
assert_eq!(g(0.0, 6), "0");
|
||||
assert_eq!(g(f64::NAN, 6), "nan");
|
||||
assert_eq!(fixed(380.0, 2), "380.00");
|
||||
assert_eq!(fixed(0.125, 2), "0.12");
|
||||
assert_eq!(fixed(0.375, 2), "0.38");
|
||||
assert_eq!(fixed(2.5, 0), "2");
|
||||
assert_eq!(fixed(0.5, 0), "0");
|
||||
assert_eq!(fixed(1.5, 0), "2");
|
||||
assert_eq!(fixed(-0.001, 2), "-0.00");
|
||||
assert_eq!(fixed(0.006, 2), "0.01");
|
||||
assert_eq!(fixed(99.995, 2), "100.00");
|
||||
assert_eq!(fixed(0.004, 2), "0.00");
|
||||
assert_eq!(round(83.25, 1), 83.2);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user