use std::collections::HashMap;
use zisk_common::{ChunkId, CollectSkipper};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct KindCollect {
pub count: u64,
pub skipper: CollectSkipper,
pub owns_frops: bool,
}
impl KindCollect {
fn taking(count: u64, skip: u64) -> Self {
Self { count, skipper: CollectSkipper::new(skip), owns_frops: false }
}
}
impl Default for KindCollect {
fn default() -> Self {
Self::taking(0, 0)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ChunkCollect<const K: usize> {
pub kinds: [KindCollect; K],
pub force_execute_to_end: bool,
}
impl<const K: usize> Default for ChunkCollect<K> {
fn default() -> Self {
Self { kinds: [KindCollect::default(); K], force_execute_to_end: false }
}
}
#[derive(Clone, Debug)]
pub struct AirSlot<const K: usize> {
pub airgroup_id: usize,
pub air_id: usize,
pub ops_per_instance: u64,
pub proves: [bool; K],
pub sees: [bool; K],
pub instances: u64,
}
#[derive(Debug)]
pub struct InstancePlan<const K: usize> {
pub air: usize,
pub chunks: HashMap<ChunkId, ChunkCollect<K>>,
}
pub fn distribute<const K: usize>(
ops: &[[u64; K]],
frops: &[[u64; K]],
airs: &[AirSlot<K>],
) -> Vec<InstancePlan<K>> {
debug_assert_eq!(ops.len(), frops.len());
let mut plans: Vec<InstancePlan<K>> = Vec::new();
let mut open: Vec<Option<usize>> = vec![None; airs.len()];
let mut room: Vec<u64> = vec![0; airs.len()];
let mut opened: Vec<u64> = vec![0; airs.len()];
for (chunk, kinds) in ops.iter().enumerate() {
let chunk_id = ChunkId(chunk);
let mut pending = *kinds;
for (a, air) in airs.iter().enumerate() {
for k in 0..K {
if !air.proves[k] {
continue;
}
while pending[k] > 0 {
if room[a] == 0 {
if opened[a] == air.instances {
break; }
plans.push(InstancePlan { air: a, chunks: HashMap::new() });
open[a] = Some(plans.len() - 1);
room[a] = air.ops_per_instance;
opened[a] += 1;
}
let take = pending[k].min(room[a]);
let skip = kinds[k] - pending[k];
let entry = plans[open[a].unwrap()].chunks.entry(chunk_id).or_default();
if entry.kinds[k].count == 0 {
entry.kinds[k] = KindCollect::taking(take, skip);
} else {
entry.kinds[k].count += take;
}
pending[k] -= take;
room[a] -= take;
}
}
}
assert!(
pending.iter().all(|&p| p == 0),
"chunk {chunk}: {pending:?} operations left unplaced; the instance counts and this \
hand-out disagree"
);
}
name_frops_accountants(frops, airs, &mut plans);
plans
}
fn name_frops_accountants<const K: usize>(
frops: &[[u64; K]],
airs: &[AirSlot<K>],
plans: &mut Vec<InstancePlan<K>>,
) {
for (chunk, kinds) in frops.iter().enumerate() {
let chunk_id = ChunkId(chunk);
for (k, &count) in kinds.iter().enumerate() {
if count == 0 {
continue;
}
let last_taker = plans
.iter()
.enumerate()
.rfind(|(_, p)| p.chunks.get(&chunk_id).is_some_and(|c| c.kinds[k].count > 0))
.map(|(i, _)| i);
let accountant = last_taker
.or_else(|| {
plans
.iter()
.enumerate()
.rfind(|(_, p)| airs[p.air].sees[k] && p.chunks.contains_key(&chunk_id))
.map(|(i, _)| i)
})
.or_else(|| {
let owner_air = airs.iter().position(|a| a.sees[k] && a.instances > 0)?;
if let Some((i, _)) =
plans.iter().enumerate().rfind(|(_, p)| p.air == owner_air)
{
return Some(i);
}
plans.push(InstancePlan { air: owner_air, chunks: HashMap::new() });
Some(plans.len() - 1)
});
let Some(accountant) = accountant else {
panic!(
"chunk {chunk}: kind {k} has {count} frequent operations but no instance can \
account for them"
);
};
let entry = plans[accountant].chunks.entry(chunk_id).or_default();
entry.kinds[k].owns_frops = true;
entry.force_execute_to_end = true;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const PACKED: usize = 0;
const DEDICATED: usize = 1;
const GENERAL: usize = 2;
fn air<const K: usize>(
air_id: usize,
cap: u64,
proves: [bool; K],
instances: u64,
) -> AirSlot<K> {
AirSlot { airgroup_id: 0, air_id, ops_per_instance: cap, proves, sees: proves, instances }
}
fn collected<const K: usize>(plans: &[InstancePlan<K>], chunks: usize) -> Vec<[u64; K]> {
let mut totals = vec![[0u64; K]; chunks];
for plan in plans {
for (chunk_id, c) in plan.chunks.iter() {
for (total, kind) in totals[chunk_id.0].iter_mut().zip(&c.kinds) {
*total += kind.count;
}
}
}
totals
}
fn assert_tiles<const K: usize>(plans: &[InstancePlan<K>], ops: &[[u64; K]]) {
assert_eq!(collected(plans, ops.len()), ops, "operations lost or duplicated");
for (chunk, kinds) in ops.iter().enumerate() {
for (k, &expected) in kinds.iter().enumerate() {
let mut ranges: Vec<(u64, u64)> = plans
.iter()
.filter_map(|p| p.chunks.get(&ChunkId(chunk)))
.filter(|c| c.kinds[k].count > 0)
.map(|c| (c.kinds[k].skipper.skip, c.kinds[k].count))
.collect();
ranges.sort();
let mut at = 0;
for (skip, count) in ranges {
assert_eq!(skip, at, "chunk {chunk} kind {k}: gap or overlap at {skip}");
at += count;
}
assert_eq!(at, expected, "chunk {chunk} kind {k}: not fully covered");
}
}
}
#[test]
fn a_residual_flows_on_and_may_split() {
let ops = [[0, 10, 0], [0, 10, 0]];
let frops = [[0, 0, 0], [0, 0, 0]];
let airs = [
air(PACKED, 7, [false, true, false], 1),
air(DEDICATED, 5, [false, true, true], 1),
air(GENERAL, 100, [true, true, true], 1),
];
let plans = distribute(&ops, &frops, &airs);
assert_tiles(&plans, &ops);
let per_air = |id: usize| -> u64 {
plans
.iter()
.filter(|p| airs[p.air].air_id == id)
.flat_map(|p| p.chunks.values())
.map(|c| c.kinds[1].count)
.sum()
};
assert_eq!(per_air(PACKED), 7);
assert_eq!(per_air(DEDICATED), 5);
assert_eq!(per_air(GENERAL), 8);
}
#[test]
fn an_air_never_takes_a_kind_it_cannot_prove() {
let ops = [[4, 0, 6]];
let frops = [[0, 0, 0]];
let airs = [
air(PACKED, 100, [false, true, false], 1), air(GENERAL, 100, [true, false, true], 1),
];
let plans = distribute(&ops, &frops, &airs);
assert_tiles(&plans, &ops);
assert!(
plans.iter().all(|p| airs[p.air].air_id != PACKED),
"the packed air must not open an instance for kinds it cannot prove"
);
}
#[test]
fn instances_fill_in_order() {
let ops = [[0, 0, 25]];
let frops = [[0, 0, 0]];
let airs = [air(DEDICATED, 10, [false, false, true], 3)];
let plans = distribute(&ops, &frops, &airs);
assert_tiles(&plans, &ops);
assert_eq!(plans.len(), 3);
let counts: Vec<u64> = plans.iter().map(|p| p.chunks[&ChunkId(0)].kinds[2].count).collect();
assert_eq!(counts, vec![10, 10, 5]);
}
#[test]
fn exactly_one_accountant_per_chunk_and_kind() {
let ops = [[3, 4, 0], [0, 0, 0]];
let frops = [[2, 1, 5], [7, 0, 0]];
let airs =
[air(PACKED, 2, [false, true, false], 1), air(GENERAL, 100, [true, true, true], 1)];
let plans = distribute(&ops, &frops, &airs);
assert_tiles(&plans, &ops);
for (chunk, kinds) in frops.iter().enumerate() {
for (k, &count) in kinds.iter().enumerate() {
let owners = plans
.iter()
.filter(|p| {
p.chunks.get(&ChunkId(chunk)).is_some_and(|c| c.kinds[k].owns_frops)
})
.count();
assert_eq!(
owners,
usize::from(count > 0),
"chunk {chunk} kind {k} has {owners} accountants for {count} frops"
);
}
}
for plan in &plans {
for c in plan.chunks.values() {
if c.kinds.iter().any(|k| k.owns_frops) {
assert!(c.force_execute_to_end);
}
}
}
}
#[test]
fn frops_without_operations_still_get_an_accountant() {
let ops = [[0, 0, 0]];
let frops = [[0, 4, 0]];
let airs =
[air(PACKED, 10, [false, true, false], 1), air(GENERAL, 10, [true, true, true], 0)];
let plans = distribute(&ops, &frops, &airs);
let owners = plans
.iter()
.filter(|p| p.chunks.get(&ChunkId(0)).is_some_and(|c| c.kinds[1].owns_frops))
.count();
assert_eq!(owners, 1, "exactly one instance must account for them");
}
#[test]
fn an_air_accounts_for_a_kind_it_cannot_prove() {
let ops = [[0, 6, 0]];
let frops = [[0, 0, 3]];
let airs = [
AirSlot {
airgroup_id: 0,
air_id: PACKED,
ops_per_instance: 10,
proves: [false, true, false],
sees: [false, true, true],
instances: 1,
},
AirSlot {
airgroup_id: 0,
air_id: DEDICATED,
ops_per_instance: 10,
proves: [false, true, true],
sees: [false, true, true],
instances: 0,
},
];
let plans = distribute(&ops, &frops, &airs);
assert_eq!(plans.len(), 1, "no instance is opened just for the frops");
let c = plans[0].chunks[&ChunkId(0)];
assert!(c.kinds[2].owns_frops, "the packed instance accounts for the full-shape frops");
assert_eq!(c.kinds[2].count, 0, "without collecting any of them");
assert!(c.force_execute_to_end);
}
#[test]
fn a_frops_only_chunk_is_added_to_an_existing_instance() {
let ops = [[0, 4, 0], [0, 0, 0], [0, 0, 0]];
let frops = [[0, 0, 0], [0, 0, 0], [0, 3, 0]];
let airs = [air(PACKED, 10, [false, true, false], 1)];
let plans = distribute(&ops, &frops, &airs);
assert_eq!(plans.len(), 1, "no extra instance is opened for the frops-only chunk");
let c = plans[0].chunks[&ChunkId(2)];
assert_eq!(c.kinds[1].count, 0, "there is nothing to collect there");
assert!(c.kinds[1].owns_frops, "but it does account for its frops");
assert!(c.force_execute_to_end, "so it has to walk the chunk");
}
#[test]
fn the_accountant_is_an_instance_already_walking_the_chunk() {
let ops = [[0, 0, 5], [0, 6, 0]];
let frops = [[0, 2, 0], [0, 0, 0]];
let airs = [
air(PACKED, 10, [false, true, false], 1), AirSlot {
airgroup_id: 0,
air_id: GENERAL,
ops_per_instance: 10,
proves: [true, true, true],
sees: [true, true, true],
instances: 1,
}, ];
let plans = distribute(&ops, &frops, &airs);
let walks = |i: usize, chunk: usize| plans[i].chunks.contains_key(&ChunkId(chunk));
let accountant = plans
.iter()
.position(|p| p.chunks.get(&ChunkId(0)).is_some_and(|c| c.kinds[1].owns_frops))
.expect("the frops must have an accountant");
assert!(walks(accountant, 0), "and it was already walking that chunk");
for (i, plan) in plans.iter().enumerate() {
let has_ops = |chunk: usize| {
plan.chunks
.get(&ChunkId(chunk))
.is_some_and(|c| c.kinds.iter().any(|k| k.count > 0))
};
for chunk in 0..2 {
if walks(i, chunk) && !has_ops(chunk) {
assert_eq!(
i, accountant,
"only the accountant may walk a chunk it collects none of"
);
}
}
}
}
#[test]
fn at_most_one_instance_is_opened_for_frops() {
let ops = [[0, 0, 0]];
let frops = [[5, 3, 2]];
let airs = [
air(PACKED, 10, [false, true, false], 0),
air(DEDICATED, 10, [false, true, true], 0),
AirSlot {
airgroup_id: 0,
air_id: GENERAL,
ops_per_instance: 10,
proves: [true, true, true],
sees: [true, true, true],
instances: 1,
},
];
let plans = distribute(&ops, &frops, &airs);
assert_eq!(plans.len(), 1, "one instance accounts for all three kinds");
let c = plans[0].chunks[&ChunkId(0)];
assert!(c.kinds.iter().all(|k| k.owns_frops), "for every kind that has frops");
}
#[test]
#[should_panic(expected = "left unplaced")]
fn too_few_instances_is_an_error() {
let ops = [[0, 0, 30]];
let frops = [[0, 0, 0]];
let airs = [air(DEDICATED, 10, [false, false, true], 2)];
distribute(&ops, &frops, &airs);
}
}