use crate::indexer::StageIndexer;
#[derive(Debug, Clone)]
pub struct PatchBuffer {
pub indices: Vec<usize>,
pub lower: Vec<f64>,
pub upper: Vec<f64>,
hydro_count: usize,
max_par_order: usize,
load_bus_count: usize,
max_blocks: usize,
active_load_patches: usize,
active_z_inflow_patches: usize,
}
impl PatchBuffer {
#[must_use]
pub fn new(
hydro_count: usize,
max_par_order: usize,
n_load_buses: usize,
max_blocks: usize,
) -> Self {
let capacity = hydro_count * (2 + max_par_order) + n_load_buses * max_blocks + hydro_count;
Self {
indices: vec![0; capacity],
lower: vec![0.0; capacity],
upper: vec![0.0; capacity],
hydro_count,
max_par_order,
load_bus_count: n_load_buses,
max_blocks,
active_load_patches: 0,
active_z_inflow_patches: 0,
}
}
pub fn fill_forward_patches(
&mut self,
indexer: &StageIndexer,
state: &[f64],
noise: &[f64],
base_row: usize,
row_scale: &[f64],
) {
debug_assert_eq!(
state.len(),
indexer.n_state,
"state slice length {got} != n_state {expected}",
got = state.len(),
expected = indexer.n_state,
);
debug_assert!(
noise.len() == indexer.hydro_count || noise.is_empty(),
"noise slice length {got} must equal hydro_count {expected} or be empty",
got = noise.len(),
expected = indexer.hydro_count,
);
let n = self.hydro_count;
let l = self.max_par_order;
for (h, &sv) in state[..n].iter().enumerate() {
self.indices[h] = h;
let scaled = if row_scale.is_empty() {
sv
} else {
sv * row_scale[h]
};
self.lower[h] = scaled;
self.upper[h] = scaled;
}
for lag in 0..l {
for h in 0..n {
let slot = n + lag * n + h;
self.indices[slot] = slot;
let sv = state[slot];
let scaled = if row_scale.is_empty() {
sv
} else {
sv * row_scale[slot]
};
self.lower[slot] = scaled;
self.upper[slot] = scaled;
}
}
let cat3_start = n * (1 + l);
for (h, &nv) in noise.iter().enumerate() {
let slot = cat3_start + h;
self.indices[slot] = ar_dynamics_row_offset(base_row, h);
self.lower[slot] = nv;
self.upper[slot] = nv;
}
}
pub fn fill_state_patches(&mut self, indexer: &StageIndexer, state: &[f64], row_scale: &[f64]) {
debug_assert_eq!(
state.len(),
indexer.n_state,
"state slice length {got} != n_state {expected}",
got = state.len(),
expected = indexer.n_state,
);
let n = self.hydro_count;
let l = self.max_par_order;
for (h, &sv) in state[..n].iter().enumerate() {
self.indices[h] = h;
let scaled = if row_scale.is_empty() {
sv
} else {
sv * row_scale[h]
};
self.lower[h] = scaled;
self.upper[h] = scaled;
}
for lag in 0..l {
for h in 0..n {
let slot = n + lag * n + h;
self.indices[slot] = slot;
let sv = state[slot];
let scaled = if row_scale.is_empty() {
sv
} else {
sv * row_scale[slot]
};
self.lower[slot] = scaled;
self.upper[slot] = scaled;
}
}
}
pub fn fill_load_patches(
&mut self,
load_row_start: usize,
n_blocks: usize,
load_rhs: &[f64],
bus_positions: &[usize],
row_scale: &[f64],
) {
debug_assert_eq!(
load_rhs.len(),
self.load_bus_count * n_blocks,
"load_rhs length {got} != load_bus_count*n_blocks {expected}",
got = load_rhs.len(),
expected = self.load_bus_count * n_blocks,
);
debug_assert_eq!(
bus_positions.len(),
self.load_bus_count,
"bus_positions length {got} != load_bus_count {expected}",
got = bus_positions.len(),
expected = self.load_bus_count,
);
debug_assert!(
n_blocks <= self.max_blocks,
"n_blocks {n_blocks} exceeds max_blocks {mb}",
mb = self.max_blocks,
);
let cat4_start = self.hydro_count * (2 + self.max_par_order);
let mut slot = cat4_start;
for (i, &bus_pos) in bus_positions.iter().enumerate() {
for blk in 0..n_blocks {
let row = load_row_start + bus_pos * n_blocks + blk;
let rhs = load_rhs[i * n_blocks + blk];
let scaled = if row_scale.is_empty() {
rhs
} else {
rhs * row_scale[row]
};
self.indices[slot] = row;
self.lower[slot] = scaled;
self.upper[slot] = scaled;
slot += 1;
}
}
self.active_load_patches = self.load_bus_count * n_blocks;
}
pub fn fill_z_inflow_patches(
&mut self,
z_inflow_row_start: usize,
z_inflow_rhs: &[f64],
row_scale: &[f64],
) {
let n = self.hydro_count;
if n == 0 || z_inflow_rhs.is_empty() {
self.active_z_inflow_patches = 0;
return;
}
let cat5_start = self.hydro_count * (2 + self.max_par_order) + self.active_load_patches;
for (h, &rhs) in z_inflow_rhs.iter().enumerate().take(n) {
let slot = cat5_start + h;
let row = z_inflow_row_start + h;
let scaled = if row_scale.is_empty() {
rhs
} else {
rhs * row_scale[row]
};
self.indices[slot] = row;
self.lower[slot] = scaled;
self.upper[slot] = scaled;
}
self.active_z_inflow_patches = n;
}
#[must_use]
#[inline]
pub fn forward_patch_count(&self) -> usize {
self.hydro_count * (2 + self.max_par_order)
+ self.active_load_patches
+ self.active_z_inflow_patches
}
#[must_use]
#[inline]
pub fn state_patch_count(&self) -> usize {
self.hydro_count * (1 + self.max_par_order)
}
}
#[must_use]
#[inline]
pub fn ar_dynamics_row_offset(base_row: usize, hydro_index: usize) -> usize {
base_row + hydro_index
}
#[cfg(test)]
#[allow(
clippy::doc_markdown,
clippy::too_many_lines,
clippy::cast_sign_loss,
clippy::cast_possible_truncation
)]
mod tests {
use super::{PatchBuffer, ar_dynamics_row_offset};
use crate::indexer::StageIndexer;
fn idx(n: usize, l: usize) -> StageIndexer {
StageIndexer::new(n, l)
}
#[test]
fn new_3_2_sizes_to_15() {
let buf = PatchBuffer::new(3, 2, 0, 0);
assert_eq!(buf.indices.len(), 15);
assert_eq!(buf.lower.len(), 15);
assert_eq!(buf.upper.len(), 15);
}
#[test]
fn new_160_12_sizes_to_2400() {
let buf = PatchBuffer::new(160, 12, 0, 0);
assert_eq!(buf.indices.len(), 2400);
assert_eq!(buf.lower.len(), 2400);
assert_eq!(buf.upper.len(), 2400);
}
#[test]
fn new_zero_lags_sizes_to_3n() {
let buf = PatchBuffer::new(5, 0, 0, 0);
assert_eq!(buf.indices.len(), 15); }
#[test]
fn new_zero_hydros_sizes_to_zero() {
let buf = PatchBuffer::new(0, 0, 0, 0);
assert_eq!(buf.indices.len(), 0);
}
#[test]
fn forward_patch_count_without_z_inflow_fill() {
let buf = PatchBuffer::new(3, 2, 0, 0);
assert_eq!(buf.forward_patch_count(), 12);
}
#[test]
fn state_patch_count_is_n_times_one_plus_l() {
let buf = PatchBuffer::new(3, 2, 0, 0);
assert_eq!(buf.state_patch_count(), 9);
}
#[test]
fn state_patch_count_zero_lags() {
let buf = PatchBuffer::new(4, 0, 0, 0);
assert_eq!(buf.state_patch_count(), 4);
}
#[test]
fn ar_dynamics_row_offset_adds_base_plus_hydro() {
assert_eq!(ar_dynamics_row_offset(100, 0), 100);
assert_eq!(ar_dynamics_row_offset(100, 1), 101);
assert_eq!(ar_dynamics_row_offset(100, 2), 102);
}
#[test]
fn ar_dynamics_row_offset_zero_base() {
assert_eq!(ar_dynamics_row_offset(0, 7), 7);
}
#[test]
fn fill_forward_patches_category1_indices() {
let mut buf = PatchBuffer::new(3, 2, 0, 0);
let state = [10.0, 20.0, 30.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let noise = [0.1, 0.2, 0.3];
buf.fill_forward_patches(&idx(3, 2), &state, &noise, 50, &[]);
assert_eq!(buf.indices[0], 0);
assert_eq!(buf.indices[1], 1);
assert_eq!(buf.indices[2], 2);
}
#[test]
fn fill_forward_patches_category2_indices() {
let mut buf = PatchBuffer::new(3, 2, 0, 0);
let state = [10.0, 20.0, 30.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let noise = [0.1, 0.2, 0.3];
buf.fill_forward_patches(&idx(3, 2), &state, &noise, 50, &[]);
assert_eq!(buf.indices[3], 3); assert_eq!(buf.indices[4], 4); assert_eq!(buf.indices[5], 5); assert_eq!(buf.indices[6], 6); assert_eq!(buf.indices[7], 7); assert_eq!(buf.indices[8], 8); }
#[test]
fn fill_forward_patches_category3_indices() {
let mut buf = PatchBuffer::new(3, 2, 0, 0);
let state = [10.0, 20.0, 30.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let noise = [0.1, 0.2, 0.3];
buf.fill_forward_patches(&idx(3, 2), &state, &noise, 50, &[]);
assert_eq!(buf.indices[9], 50); assert_eq!(buf.indices[10], 51); assert_eq!(buf.indices[11], 52); }
#[test]
fn fill_forward_patches_category1_values() {
let mut buf = PatchBuffer::new(3, 2, 0, 0);
let state = [10.0, 20.0, 30.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let noise = [0.1, 0.2, 0.3];
buf.fill_forward_patches(&idx(3, 2), &state, &noise, 50, &[]);
assert_eq!(buf.lower[0], 10.0);
assert_eq!(buf.upper[0], 10.0);
assert_eq!(buf.lower[1], 20.0);
assert_eq!(buf.upper[1], 20.0);
assert_eq!(buf.lower[2], 30.0);
assert_eq!(buf.upper[2], 30.0);
}
#[test]
fn fill_forward_patches_category2_values() {
let mut buf = PatchBuffer::new(3, 2, 0, 0);
let state = [10.0, 20.0, 30.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let noise = [0.1, 0.2, 0.3];
buf.fill_forward_patches(&idx(3, 2), &state, &noise, 50, &[]);
assert_eq!(buf.lower[3], 1.0);
assert_eq!(buf.upper[3], 1.0);
assert_eq!(buf.lower[4], 2.0);
assert_eq!(buf.upper[4], 2.0);
assert_eq!(buf.lower[5], 3.0);
assert_eq!(buf.upper[5], 3.0);
assert_eq!(buf.lower[6], 4.0);
assert_eq!(buf.upper[6], 4.0);
assert_eq!(buf.lower[7], 5.0);
assert_eq!(buf.upper[7], 5.0);
assert_eq!(buf.lower[8], 6.0);
assert_eq!(buf.upper[8], 6.0);
}
#[test]
fn fill_forward_patches_category3_values() {
let mut buf = PatchBuffer::new(3, 2, 0, 0);
let state = [10.0, 20.0, 30.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let noise = [0.1, 0.2, 0.3];
buf.fill_forward_patches(&idx(3, 2), &state, &noise, 50, &[]);
assert_eq!(buf.lower[9], 0.1);
assert_eq!(buf.upper[9], 0.1);
assert_eq!(buf.lower[10], 0.2);
assert_eq!(buf.upper[10], 0.2);
assert_eq!(buf.lower[11], 0.3);
assert_eq!(buf.upper[11], 0.3);
}
#[test]
fn fill_forward_patches_all_equality_constraints() {
let mut buf = PatchBuffer::new(3, 2, 0, 0);
let state = [10.0, 20.0, 30.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let noise = [0.1, 0.2, 0.3];
buf.fill_forward_patches(&idx(3, 2), &state, &noise, 50, &[]);
for i in 0..buf.forward_patch_count() {
assert_eq!(
buf.lower[i],
buf.upper[i],
"patch {i}: lower {lo} != upper {up}",
lo = buf.lower[i],
up = buf.upper[i],
);
}
}
#[test]
fn fill_state_patches_count_is_n_times_one_plus_l() {
let mut buf = PatchBuffer::new(3, 2, 0, 0);
let state = [10.0, 20.0, 30.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
buf.fill_state_patches(&idx(3, 2), &state, &[]);
assert_eq!(buf.state_patch_count(), 9);
}
#[test]
fn fill_state_patches_category1_correct() {
let mut buf = PatchBuffer::new(3, 2, 0, 0);
let state = [10.0, 20.0, 30.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
buf.fill_state_patches(&idx(3, 2), &state, &[]);
assert_eq!(buf.indices[0], 0);
assert_eq!(buf.lower[0], 10.0);
assert_eq!(buf.upper[0], 10.0);
assert_eq!(buf.indices[1], 1);
assert_eq!(buf.lower[1], 20.0);
assert_eq!(buf.upper[1], 20.0);
assert_eq!(buf.indices[2], 2);
assert_eq!(buf.lower[2], 30.0);
assert_eq!(buf.upper[2], 30.0);
}
#[test]
fn fill_state_patches_category2_correct() {
let mut buf = PatchBuffer::new(3, 2, 0, 0);
let state = [10.0, 20.0, 30.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
buf.fill_state_patches(&idx(3, 2), &state, &[]);
assert_eq!(buf.indices[3], 3);
assert_eq!(buf.lower[3], 1.0);
assert_eq!(buf.upper[3], 1.0);
assert_eq!(buf.indices[8], 8);
assert_eq!(buf.lower[8], 6.0);
assert_eq!(buf.upper[8], 6.0);
}
#[test]
fn fill_state_patches_equality_constraints_in_active_range() {
let mut buf = PatchBuffer::new(3, 2, 0, 0);
let state = [10.0, 20.0, 30.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
buf.fill_state_patches(&idx(3, 2), &state, &[]);
let active = buf.state_patch_count();
for i in 0..active {
assert_eq!(
buf.lower[i],
buf.upper[i],
"state patch {i}: lower {lo} != upper {up}",
lo = buf.lower[i],
up = buf.upper[i],
);
}
}
#[test]
fn forward_patches_zero_lags_only_storage_and_noise() {
let n = 2;
let mut buf = PatchBuffer::new(n, 0, 0, 0);
let state = [5.0, 7.0]; let noise = [0.5, 0.6];
buf.fill_forward_patches(&idx(n, 0), &state, &noise, 10, &[]);
assert_eq!(buf.forward_patch_count(), 4);
assert_eq!(buf.indices[0], 0);
assert_eq!(buf.lower[0], 5.0);
assert_eq!(buf.indices[1], 1);
assert_eq!(buf.lower[1], 7.0);
assert_eq!(buf.indices[2], 10); assert_eq!(buf.lower[2], 0.5);
assert_eq!(buf.indices[3], 11); assert_eq!(buf.lower[3], 0.6);
}
#[test]
fn state_patches_zero_lags_only_storage() {
let n = 3;
let mut buf = PatchBuffer::new(n, 0, 0, 0);
let state = [1.0, 2.0, 3.0]; buf.fill_state_patches(&idx(n, 0), &state, &[]);
assert_eq!(buf.state_patch_count(), 3);
assert_eq!(buf.indices[0], 0);
assert_eq!(buf.lower[0], 1.0);
assert_eq!(buf.upper[0], 1.0);
assert_eq!(buf.indices[1], 1);
assert_eq!(buf.lower[1], 2.0);
assert_eq!(buf.indices[2], 2);
assert_eq!(buf.lower[2], 3.0);
}
#[test]
fn production_scale_forward_patch_count() {
let buf = PatchBuffer::new(160, 12, 0, 0);
assert_eq!(buf.forward_patch_count(), 2240);
assert_eq!(buf.indices.len(), 2400);
}
#[test]
#[allow(clippy::cast_precision_loss)] fn production_scale_fill_forward_patches_smoke() {
let n = 160;
let l = 12;
let mut buf = PatchBuffer::new(n, l, 0, 0);
let n_state = n * (1 + l);
let state: Vec<f64> = (0..n_state).map(|i| i as f64).collect();
let noise: Vec<f64> = (0..n).map(|h| h as f64 * 0.01).collect();
buf.fill_forward_patches(&StageIndexer::new(n, l), &state, &noise, 500, &[]);
assert_eq!(buf.indices[0], 0);
assert_eq!(buf.lower[0], 0.0);
assert_eq!(buf.indices[2080], 500); assert_eq!(buf.lower[2080], 0.0); assert_eq!(buf.indices[2239], 659); assert_eq!(buf.lower[2239], 159.0 * 0.01);
for i in 0..buf.forward_patch_count() {
assert_eq!(buf.lower[i], buf.upper[i], "patch {i} not equality");
}
}
#[test]
fn clone_and_debug() {
let buf = PatchBuffer::new(3, 2, 0, 0);
let cloned = buf.clone();
assert_eq!(cloned.indices.len(), buf.indices.len());
let s = format!("{buf:?}");
assert!(s.contains("PatchBuffer"));
}
#[test]
fn new_with_load_allocates_correct_capacity() {
let buf = PatchBuffer::new(2, 1, 1, 3);
assert_eq!(buf.indices.len(), 11);
assert_eq!(buf.lower.len(), 11);
assert_eq!(buf.upper.len(), 11);
}
#[test]
fn fill_load_patches_correct_indices() {
let mut buf = PatchBuffer::new(0, 0, 2, 2);
let load_rhs = [300.0_f64, 280.0, 500.0, 450.0];
let bus_positions = [0_usize, 1];
buf.fill_load_patches(100, 2, &load_rhs, &bus_positions, &[]);
assert_eq!(buf.indices[0], 100); assert_eq!(buf.indices[1], 101); assert_eq!(buf.indices[2], 102); assert_eq!(buf.indices[3], 103); }
#[test]
fn fill_load_patches_correct_values() {
let mut buf = PatchBuffer::new(0, 0, 2, 2);
let load_rhs = [300.0_f64, 280.0, 500.0, 450.0];
let bus_positions = [0_usize, 1];
buf.fill_load_patches(100, 2, &load_rhs, &bus_positions, &[]);
assert_eq!(buf.lower[0], 300.0);
assert_eq!(buf.upper[0], 300.0);
assert_eq!(buf.lower[1], 280.0);
assert_eq!(buf.upper[1], 280.0);
assert_eq!(buf.lower[2], 500.0);
assert_eq!(buf.upper[2], 500.0);
assert_eq!(buf.lower[3], 450.0);
assert_eq!(buf.upper[3], 450.0);
}
#[test]
fn fill_load_patches_equality_constraints() {
let mut buf = PatchBuffer::new(3, 2, 2, 3);
let state = [10.0, 20.0, 30.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let noise = [0.1, 0.2, 0.3];
buf.fill_forward_patches(&idx(3, 2), &state, &noise, 50, &[]);
let load_rhs = [100.0_f64, 90.0, 80.0, 200.0, 190.0, 180.0];
let bus_positions = [0_usize, 1];
buf.fill_load_patches(20, 3, &load_rhs, &bus_positions, &[]);
let count = buf.forward_patch_count();
for i in 0..count {
assert_eq!(
buf.lower[i],
buf.upper[i],
"patch {i}: lower {lo} != upper {up}",
lo = buf.lower[i],
up = buf.upper[i],
);
}
}
#[test]
fn forward_patch_count_includes_load() {
let mut buf = PatchBuffer::new(3, 2, 2, 3);
let state = [10.0, 20.0, 30.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let noise = [0.1, 0.2, 0.3];
buf.fill_forward_patches(&idx(3, 2), &state, &noise, 50, &[]);
let load_rhs = [100.0_f64, 90.0, 80.0, 200.0, 190.0, 180.0];
let bus_positions = [0_usize, 1];
buf.fill_load_patches(20, 3, &load_rhs, &bus_positions, &[]);
assert_eq!(buf.forward_patch_count(), 18); }
#[test]
fn state_patch_count_excludes_load() {
let mut buf = PatchBuffer::new(3, 2, 2, 3);
let state = [10.0, 20.0, 30.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
buf.fill_state_patches(&idx(3, 2), &state, &[]);
let load_rhs = [100.0_f64, 90.0, 80.0, 200.0, 190.0, 180.0];
let bus_positions = [0_usize, 1];
buf.fill_load_patches(20, 3, &load_rhs, &bus_positions, &[]);
assert_eq!(buf.state_patch_count(), 9);
}
#[test]
fn zero_load_buses_no_category4() {
let mut buf = PatchBuffer::new(3, 2, 0, 0);
let state = [10.0, 20.0, 30.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let noise = [0.1, 0.2, 0.3];
buf.fill_forward_patches(&idx(3, 2), &state, &noise, 50, &[]);
assert_eq!(buf.forward_patch_count(), 12); }
}