#![cfg(all(feature = "serde", not(target_arch = "wasm32")))]
#[path = "support/backend_aliases.rs"]
mod backend_aliases;
use basin::{
CheckpointWriter, CostFunction, Executor, FirstOrderState, Gradient,
GradientDescent, ObserverMode, State, read_checkpoint,
};
struct Quadratic;
impl basin::ProblemConstraints for Quadratic {
type Constraints = basin::NoConstraints;
fn constraint_set(
&self,
) -> <Self::Constraints as basin::ConstraintDescription>::Borrowed<'_> {
basin::Constraints::new()
}
}
impl CostFunction for Quadratic {
type Param = Vec<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, x: &Vec<f64>) -> Result<f64, std::convert::Infallible> {
Ok(0.5 * x.iter().map(|v| v * v).sum::<f64>())
}
}
impl Gradient for Quadratic {
type Gradient = Vec<f64>;
fn gradient(
&self,
x: &Vec<f64>,
) -> Result<Vec<f64>, std::convert::Infallible> {
Ok(x.clone())
}
}
#[test]
fn checkpoint_warm_start_matches_uninterrupted_run() {
let dir =
std::env::temp_dir().join(format!("basin-ckpt-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("run.ckpt");
let start = vec![5.0, -3.0, 2.0];
let step = 0.1;
let reference = Executor::new(
Quadratic,
GradientDescent::new(step),
FirstOrderState::new(start.clone()),
)
.max_iter(20)
.run()
.unwrap();
Executor::new(
Quadratic,
GradientDescent::new(step),
FirstOrderState::new(start),
)
.max_iter(12)
.observe_with(CheckpointWriter::new(&path), ObserverMode::Every(4))
.run()
.unwrap();
let reloaded: FirstOrderState<Vec<f64>> = read_checkpoint(&path).unwrap();
let bytes = std::fs::read(&path).unwrap();
assert_eq!(&bytes[..8], b"BASINST\0");
assert_eq!(&bytes[8..12], &1_u32.to_le_bytes());
assert_eq!(&bytes[12..], postcard::to_allocvec(&reloaded).unwrap());
assert_eq!(reloaded.iter(), 12);
let resumed =
Executor::new(Quadratic, GradientDescent::new(step), reloaded)
.max_iter(8)
.run()
.unwrap();
assert_eq!(resumed.iter(), 8);
assert_eq!(resumed.state.counts().cost_evals, 9);
assert_eq!(resumed.state.counts().gradient_evals, 9);
for (a, b) in resumed.param().iter().zip(reference.param()) {
assert!((a - b).abs() < 1e-12, "resumed {a} vs reference {b}");
}
assert!((resumed.cost() - reference.cost()).abs() < 1e-12);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn unprefixed_checkpoints_are_rejected() {
let path = std::env::temp_dir()
.join(format!("basin-unprefixed-{}.ckpt", std::process::id()));
let bytes = [1];
std::fs::write(&path, bytes).unwrap();
let error = read_checkpoint::<u8>(&path).unwrap_err();
assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
assert!(error.to_string().contains("magic"));
assert_eq!(std::fs::read(&path).unwrap(), bytes);
std::fs::remove_file(&path).unwrap();
}
#[test]
fn invalid_postcard_checkpoints_are_rejected() {
let path = std::env::temp_dir()
.join(format!("basin-invalid-{}.ckpt", std::process::id()));
let prefix = [b"BASINST\0".as_slice(), &1_u32.to_le_bytes()].concat();
let mut unsupported = prefix.clone();
unsupported[8..12].copy_from_slice(&2_u32.to_le_bytes());
for bytes in [
vec![],
prefix[..3].to_vec(),
[b"BASINEX\0".as_slice(), &1_u32.to_le_bytes(), &[1]].concat(),
prefix[..8].to_vec(),
prefix[..10].to_vec(),
unsupported,
prefix.clone(),
[prefix.as_slice(), &[1, 0]].concat(),
] {
std::fs::write(&path, bytes).unwrap();
let error = read_checkpoint::<u8>(&path).unwrap_err();
assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
}
std::fs::write(&path, [prefix.as_slice(), &[2]].concat()).unwrap();
assert_eq!(
read_checkpoint::<bool>(&path).unwrap_err().kind(),
std::io::ErrorKind::InvalidData
);
std::fs::remove_file(&path).unwrap();
}
fn round_trip<T: serde::Serialize + serde::de::DeserializeOwned>(
value: &T,
) -> T {
let bytes = postcard::to_allocvec(value).unwrap();
let (restored, remaining) = postcard::take_from_bytes(&bytes).unwrap();
assert!(remaining.is_empty());
restored
}
macro_rules! check_float_bits {
($float:ty, $nan_bits:expr, $vector:expr) => {{
let nan = <$float>::from_bits($nan_bits);
let values: &[$float] = &[
0.0,
-0.0,
1.5,
<$float>::INFINITY,
<$float>::NEG_INFINITY,
nan,
-nan,
];
let state = FirstOrderState::<_, $float>::new(($vector)(values));
let restored = round_trip(&state);
let expected = values
.iter()
.map(|value| value.to_bits())
.collect::<Vec<_>>();
assert_eq!(
restored
.param()
.iter()
.map(|value| value.to_bits())
.collect::<Vec<_>>(),
expected
);
}};
}
#[test]
fn postcard_preserves_vec_float_bits() {
check_float_bits!(f32, 0x7fc0_0042, |values: &[f32]| values.to_vec());
check_float_bits!(f64, 0x7ff8_0000_0000_0042, |values: &[f64]| values
.to_vec());
}
#[cfg(feature = "nalgebra_all")]
#[test]
fn postcard_preserves_nalgebra_float_bits() {
use backend_aliases::nalgebra::DVector;
check_float_bits!(f32, 0x7fc0_0042, DVector::from_column_slice);
check_float_bits!(f64, 0x7ff8_0000_0000_0042, DVector::from_column_slice);
}
#[cfg(feature = "ndarray_all")]
#[test]
fn postcard_preserves_ndarray_float_bits() {
use backend_aliases::ndarray::Array1;
check_float_bits!(f32, 0x7fc0_0042, |values: &[f32]| Array1::from_vec(
values.to_vec()
));
check_float_bits!(f64, 0x7ff8_0000_0000_0042, |values: &[f64]| {
Array1::from_vec(values.to_vec())
});
}