use core::cmp::Reverse;
use core::fmt;
use std::collections::HashMap;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Expert(pub usize);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Device(pub usize);
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Place {
at: HashMap<Expert, Device>,
}
impl FromIterator<(Expert, Device)> for Place {
fn from_iter<I: IntoIterator<Item = (Expert, Device)>>(pins: I) -> Place {
Place {
at: pins.into_iter().collect(),
}
}
}
impl Place {
pub fn device(&self, e: Expert) -> Option<Device> {
self.at.get(&e).copied()
}
pub fn pin(
mut self,
experts: &[(Expert, u64)],
devices: &[(Device, u64)],
) -> Result<Place, Error> {
let size: HashMap<Expert, u64> = experts.iter().copied().collect();
let mut free: Vec<(Device, u64)> = Vec::with_capacity(devices.len());
for &(device, have) in devices {
let need: u64 = self
.at
.iter()
.filter(|(_, &owner)| owner == device)
.filter_map(|(e, _)| size.get(e))
.sum();
if need > have {
return Err(Error::Over { device, need, have });
}
free.push((device, have - need));
}
let mut order: Vec<(Expert, u64)> = size
.iter()
.map(|(&e, &need)| (e, need))
.filter(|(e, _)| !self.at.contains_key(e))
.collect();
order.sort_by_key(|&(e, need)| (Reverse(need), e));
for (e, need) in order {
let room = free
.iter_mut()
.filter(|(_, left)| *left >= need)
.min_by_key(|(_, left)| Reverse(*left));
let Some(slot) = room else {
return Err(Error::Unplaceable { expert: e, need });
};
slot.1 -= need;
self.at.insert(e, slot.0);
}
Ok(self)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Error {
Over {
device: Device,
need: u64,
have: u64,
},
Unplaceable { expert: Expert, need: u64 },
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Error::Over { device, need, have } => write!(
f,
"device {} is declared {need} but holds {have} — split the model into smaller slabs, or declare the pin on a larger device",
device.0
),
Error::Unplaceable { expert, need } => write!(
f,
"expert {} needs {need}; no device has that much room left",
expert.0
),
}
}
}
impl std::error::Error for Error {}
#[cfg(test)]
mod tests {
use super::*;
const FLEET: [(Device, u64); 3] = [(Device(0), 128), (Device(1), 128), (Device(2), 64)];
const POOL: [(Expert, u64); 4] = [
(Expert(0), 100),
(Expert(1), 40),
(Expert(2), 40),
(Expert(3), 10),
];
#[test]
fn pin_places_the_largest_expert_first_where_there_is_most_room() {
let pi = Place::default().pin(&POOL, &FLEET).unwrap();
assert_eq!(pi.device(Expert(0)), Some(Device(0)));
assert_eq!(pi.device(Expert(1)), Some(Device(1)));
assert_eq!(pi.device(Expert(2)), Some(Device(1)));
assert_eq!(pi.device(Expert(3)), Some(Device(2)));
}
#[test]
fn a_declared_pin_is_honored_and_charged_to_its_device() {
let declared: Place = [(Expert(1), Device(2))].into_iter().collect();
let pool = [(Expert(1), 40), (Expert(4), 30)];
assert_eq!(
declared.clone().pin(&pool, &[(Device(2), 64)]),
Err(Error::Unplaceable {
expert: Expert(4),
need: 30
}),
"the declared pin's 40 was not charged to D2"
);
let pi = declared
.pin(&pool, &[(Device(2), 64), (Device(3), 64)])
.unwrap();
assert_eq!(pi.device(Expert(1)), Some(Device(2)), "declared pin moved");
assert_eq!(pi.device(Expert(4)), Some(Device(3)));
}
#[test]
fn a_pool_that_does_not_fit_is_refused_not_rounded() {
assert_eq!(
Place::default().pin(&[(Expert(0), 200)], &FLEET),
Err(Error::Unplaceable {
expert: Expert(0),
need: 200
})
);
let declared: Place = [(Expert(0), Device(2))].into_iter().collect();
assert_eq!(
declared.pin(&[(Expert(0), 100)], &[(Device(2), 64)]),
Err(Error::Over {
device: Device(2),
need: 100,
have: 64
})
);
}
#[test]
fn a_repeated_expert_id_is_charged_and_placed_once() {
let pool = [(Expert(0), 100), (Expert(0), 100), (Expert(1), 100)];
let fleet = [(Device(0), 120), (Device(1), 120)];
let pi = Place::default().pin(&pool, &fleet).unwrap();
assert_eq!(pi.device(Expert(0)), Some(Device(0)));
assert_eq!(pi.device(Expert(1)), Some(Device(1)));
assert_eq!(pi.at.len(), 2);
let reordered = [(Expert(1), 100), (Expert(0), 100), (Expert(0), 100)];
assert_eq!(Place::default().pin(&reordered, &fleet).unwrap(), pi);
}
#[test]
fn pi_resolves_an_expert_to_a_device_deterministically() {
let pi = Place::default().pin(&POOL, &FLEET).unwrap();
for _ in 0..64 {
assert_eq!(
Place::default().pin(&POOL, &FLEET).unwrap(),
pi,
"π is not a function of its inputs"
);
}
let shuffled = [
(Expert(3), 10),
(Expert(2), 40),
(Expert(0), 100),
(Expert(1), 40),
];
assert_eq!(
Place::default().pin(&shuffled, &FLEET).unwrap(),
pi,
"π depends on the order the pool was listed in"
);
assert_eq!(pi.device(Expert(0)), pi.device(Expert(0)));
assert_eq!(
pi.device(Expert(9)),
None,
"an expert outside the pool has no address"
);
}
}