Probe dependency-aware ordering across container boundaries

This commit is contained in:
2026-09-24 21:50:15 -07:00
parent c02a03c553
commit 42f595a5d6
4 changed files with 929 additions and 12 deletions
+55 -8
View File
@@ -5,6 +5,8 @@ use oa_core::{
}; };
use serde_json::{Value, json}; use serde_json::{Value, json};
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
#[path = "auto_layout_probe/boundary_fixtures.rs"]
mod boundary_fixtures;
#[path = "auto_layout_probe/clearance.rs"] #[path = "auto_layout_probe/clearance.rs"]
mod clearance; mod clearance;
#[path = "auto_layout_probe/condensed.rs"] #[path = "auto_layout_probe/condensed.rs"]
@@ -411,6 +413,7 @@ fn metrics(project: &Project, diagram: Id) -> Value {
}) })
} }
#[derive(Clone)]
struct Builder { struct Builder {
project: Project, project: Project,
diagram: Id, diagram: Id,
@@ -1252,17 +1255,35 @@ fn repair_placement_cli(project_file: &std::path::Path, diagram_text: &std::ffi:
} }
fn scc_observe(name: &str, f: Builder) -> Value { fn scc_observe(name: &str, f: Builder) -> Value {
placement_observe(name, f, false)
}
fn placement_observe(name: &str, f: Builder, boundary_order: bool) -> Value {
let before = f.project; let before = f.project;
let diagram = f.diagram; let diagram = f.diagram;
validate_candidate(&before).expect("Validate SCC input"); validate_candidate(&before).expect("Validate SCC input");
let candidate = match condensed::candidate_with_diagnostics(&before, diagram) { let with_diagnostics = |p: &Project| {
if boundary_order {
condensed::boundary_candidate_with_diagnostics(p, diagram)
} else {
condensed::candidate_with_diagnostics(p, diagram)
}
};
let propose = |p: &Project| {
if boundary_order {
condensed::boundary_candidate_with_diagnostics(p, diagram).map(|(p, _)| p)
} else {
condensed::candidate(p, diagram)
}
};
let candidate = match with_diagnostics(&before) {
Err(error) => json!({"supported":false,"error":error.to_string()}), Err(error) => json!({"supported":false,"error":error.to_string()}),
Ok((after, plan)) => { Ok((after, plan)) => {
let same = condensed::candidate(&before, diagram); let same = propose(&before);
let mut repeated = after.clone(); let mut repeated = after.clone();
let mut stability = Vec::new(); let mut stability = Vec::new();
for pass in 2..=10 { for pass in 2..=10 {
match condensed::candidate(&repeated, diagram) { match propose(&repeated) {
Ok(next) => { Ok(next) => {
stability.push(json!({"pass":pass,"equal_previous":next==repeated,"equal_first":next==after})); stability.push(json!({"pass":pass,"equal_previous":next==repeated,"equal_first":next==after}));
repeated = next; repeated = next;
@@ -1283,7 +1304,11 @@ fn scc_observe(name: &str, f: Builder) -> Value {
"before":metrics(&before,diagram),"candidate":candidate}) "before":metrics(&before,diagram),"candidate":candidate})
} }
fn scc_placement_cli(project_file: &std::path::Path, diagram_text: &std::ffi::OsStr) { fn scc_placement_cli(
project_file: &std::path::Path,
diagram_text: &std::ffi::OsStr,
boundary_order: bool,
) {
let project: Project = let project: Project =
serde_json::from_slice(&std::fs::read(project_file).expect("Read Project")) serde_json::from_slice(&std::fs::read(project_file).expect("Read Project"))
.expect("Decode Project"); .expect("Decode Project");
@@ -1294,14 +1319,15 @@ fn scc_placement_cli(project_file: &std::path::Path, diagram_text: &std::ffi::Os
.expect("Diagram UUID"); .expect("Diagram UUID");
println!( println!(
"{}", "{}",
scc_observe( placement_observe(
"external_case", "external_case",
Builder { Builder {
project, project,
diagram, diagram,
next: 0, next: 0,
witness: None witness: None
} },
boundary_order,
) )
); );
} }
@@ -1416,7 +1442,11 @@ fn main() {
return; return;
} }
if args.len() == 4 && args[1] == "--scc-placement" { if args.len() == 4 && args[1] == "--scc-placement" {
scc_placement_cli(std::path::Path::new(&args[2]), &args[3]); scc_placement_cli(std::path::Path::new(&args[2]), &args[3], false);
return;
}
if args.len() == 4 && args[1] == "--boundary-placement" {
scc_placement_cli(std::path::Path::new(&args[2]), &args[3], true);
return; return;
} }
if args.len() == 2 && args[1] == "--scc-fixtures" { if args.len() == 2 && args[1] == "--scc-fixtures" {
@@ -1425,10 +1455,27 @@ fn main() {
} }
return; return;
} }
if args.len() == 2 && args[1] == "--boundary-fixtures" {
for (name, f) in fixtures() {
println!("{}", placement_observe(name, f, true));
}
return;
}
if args.len() == 2 && args[1] == "--boundary-extra-fixtures" {
for (name, f) in boundary_fixtures::fixtures() {
println!(
"{}",
json!({"fixture":name,
"baseline":scc_observe(name, f.clone()),
"boundary":placement_observe(name, f, true)})
);
}
return;
}
assert_eq!( assert_eq!(
args.len(), args.len(),
1, 1,
"Usage: auto_layout_probe [--route-corpus MANIFEST | --arrange-corpus MANIFEST | --import-placement PROJECT UUID PLAN | --repair-placement PROJECT UUID | --scc-placement PROJECT UUID | --scc-fixtures]" "Usage: auto_layout_probe [--route-corpus MANIFEST | --arrange-corpus MANIFEST | --import-placement PROJECT UUID PLAN | --repair-placement PROJECT UUID | --scc-placement PROJECT UUID | --scc-fixtures | --boundary-placement PROJECT UUID | --boundary-fixtures | --boundary-extra-fixtures]"
); );
println!( println!(
"{}", "{}",
@@ -0,0 +1,131 @@
//! Independent structural falsifiers for incoming boundary ordering.
use super::*;
fn pair(f: &mut Builder, name: &str, x: f32) -> (Id, Id, Id) {
let parent = f.function(name, None, x, 40.0);
let a = f.function(&format!("{name} A"), Some(parent), x + 60.0, 130.0);
let b = f.function(&format!("{name} B"), Some(parent), x + 60.0, 330.0);
(parent, a, b)
}
pub(super) fn fixtures() -> Vec<(&'static str, Builder)> {
let mut all = Vec::new();
let mut f = Builder::new("boundary_opposing_parents");
let (_, la, lb) = pair(&mut f, "Left", 40.0);
let (_, ra, rb) = pair(&mut f, "Right", 820.0);
for (name, a, b) in [
("Reverse upper", la, rb),
("Reverse lower", lb, ra),
("Return upper", ra, la),
("Return lower", rb, lb),
] {
f.exchange(name, a, b);
}
all.push(("boundary_opposing_parents", f));
let mut f = Builder::new("boundary_three_parent_chain");
let (_, la, lb) = pair(&mut f, "Left", 40.0);
let (middle, ma, mb) = pair(&mut f, "Middle", 820.0);
let (_, ra, rb) = pair(&mut f, "Right", 1600.0);
for (name, a, b) in [
("Left upper", la, mb),
("Left lower", lb, ma),
("Middle upper", mb, ra),
("Middle lower", ma, rb),
] {
f.exchange(name, a, b);
}
let mut translated = f.clone();
for element in [middle, ma, mb] {
let node = translated.node(element);
let position = &mut translated
.project
.diagrams
.get_mut(&translated.diagram)
.unwrap()
.nodes
.get_mut(&node)
.unwrap()
.position;
position.x += 300.0;
position.y += 500.0;
}
let mut permuted = f.clone();
let ids: Vec<_> = permuted.project.relations.keys().copied().collect();
for (i, id) in ids.iter().enumerate() {
reidentify(
&mut permuted.project,
*id,
Id::from_u128(1_000_000 + i as u128),
)
.unwrap();
}
for (i, id) in ids.iter().rev().enumerate() {
reidentify(
&mut permuted.project,
Id::from_u128(1_000_000 + i as u128),
*id,
)
.unwrap();
}
all.push(("boundary_three_parent_chain", f));
all.push(("boundary_three_parent_translated", translated));
all.push(("boundary_three_parent_edge_ids_reversed", permuted));
let mut f = Builder::new("boundary_two_local_sccs");
let (_, sa, sb) = pair(&mut f, "Source", 40.0);
let target = f.function("Target", None, 820.0, 40.0);
let x1 = f.function("X1", Some(target), 880.0, 130.0);
let x2 = f.function("X2", Some(target), 1200.0, 130.0);
let y1 = f.function("Y1", Some(target), 880.0, 330.0);
let y2 = f.function("Y2", Some(target), 1200.0, 330.0);
let z = f.function("Z", Some(target), 1520.0, 230.0);
for (name, a, b) in [
("X forward", x1, x2),
("X back", x2, x1),
("Y forward", y1, y2),
("Y back", y2, y1),
("X result", x2, z),
("Y result", y2, z),
("Upper boundary", sa, y1),
("Lower boundary", sb, x1),
] {
f.exchange(name, a, b);
}
all.push(("boundary_two_local_sccs", f));
let mut f = super::fixtures()
.into_iter()
.find(|(name, _)| *name == "two_containers_reversed_children")
.unwrap()
.1;
let relations: Vec<_> = f.project.relations.values().cloned().collect();
for relation in relations {
if let RelationBody::FunctionalExchange {
source_port,
target_port,
..
} = relation.body
{
for (port, side, offset) in [
(source_port, PortAnchorSide::Right, 0.3),
(target_port, PortAnchorSide::Left, 0.7),
] {
let owner = f.project.elements[&port].parent.unwrap();
let node = f.node(owner);
f.project = prepare_command(
&f.project,
&Command::SetPortAnchor {
diagram: f.diagram,
node,
anchor: PortAnchor { port, side, offset },
},
)
.unwrap()
.project;
}
}
}
all.push(("boundary_two_containers_anchored", f));
all
}
@@ -7,6 +7,9 @@ const LIMIT: usize = 20_000_000;
const PAD: f32 = 32.0; const PAD: f32 = 32.0;
const SPAN: f32 = 100_000.0; const SPAN: f32 = 100_000.0;
#[path = "condensed/boundary.rs"]
mod boundary;
fn error(text: &str) -> ModelError { fn error(text: &str) -> ModelError {
ModelError(format!("SCC placement: {text}")) ModelError(format!("SCC placement: {text}"))
} }
@@ -161,6 +164,21 @@ pub fn candidate(project: &Project, diagram: Id) -> Result<Project, ModelError>
pub fn candidate_with_diagnostics( pub fn candidate_with_diagnostics(
project: &Project, project: &Project,
diagram: Id, diagram: Id,
) -> Result<(Project, serde_json::Value), ModelError> {
candidate_policy(project, diagram, false)
}
pub fn boundary_candidate_with_diagnostics(
project: &Project,
diagram: Id,
) -> Result<(Project, serde_json::Value), ModelError> {
candidate_policy(project, diagram, true)
}
fn candidate_policy(
project: &Project,
diagram: Id,
order_boundaries: bool,
) -> Result<(Project, serde_json::Value), ModelError> { ) -> Result<(Project, serde_json::Value), ModelError> {
let d = project let d = project
.diagrams .diagrams
@@ -217,6 +235,7 @@ pub fn candidate_with_diagnostics(
let mut sizes: BTreeMap<_, _> = view.nodes.iter().map(|n| (n.id, n.size)).collect(); let mut sizes: BTreeMap<_, _> = view.nodes.iter().map(|n| (n.id, n.size)).collect();
let mut offsets = BTreeMap::new(); let mut offsets = BTreeMap::new();
let mut diagnostics = Vec::new(); let mut diagnostics = Vec::new();
let mut boundary_groups = Vec::new();
let mut groups: Vec<_> = children.keys().copied().collect(); let mut groups: Vec<_> = children.keys().copied().collect();
groups.sort_by_key(|p| std::cmp::Reverse(p.map_or(0, |id| depth[&id] + 1))); groups.sort_by_key(|p| std::cmp::Reverse(p.map_or(0, |id| depth[&id] + 1)));
for parent in groups { for parent in groups {
@@ -361,6 +380,7 @@ pub fn candidate_with_diagnostics(
height: 0.0, height: 0.0,
}; };
let mut prior_order = vec![0usize; comps.len()]; let mut prior_order = vec![0usize; comps.len()];
let mut bands = Vec::new();
for rank in 0..=max_rank { for rank in 0..=max_rank {
let mut layer: Vec<_> = (0..comps.len()).filter(|&c| ranks[c] == rank).collect(); let mut layer: Vec<_> = (0..comps.len()).filter(|&c| ranks[c] == rank).collect();
// Fixed one-pass proximity order, including undirected links, without // Fixed one-pass proximity order, including undirected links, without
@@ -426,6 +446,12 @@ pub fn candidate_with_diagnostics(
} }
let mut y = 0.0; let mut y = 0.0;
let mut width = 0.0_f32; let mut width = 0.0_f32;
if order_boundaries && parent.is_some() {
bands.push(boundary::Band {
components: layer.clone(),
gap: cut.height,
});
}
for (i, c) in layer.into_iter().enumerate() { for (i, c) in layer.into_iter().enumerate() {
prior_order[c] = i; prior_order[c] = i;
block_positions[c] = Position { x, y }; block_positions[c] = Position { x, y };
@@ -464,6 +490,25 @@ pub fn candidate_with_diagnostics(
let size = sizes.get_mut(&id).unwrap(); let size = sizes.get_mut(&id).unwrap();
size.width = size.width.max(total.width + 2.0 * PAD); size.width = size.width.max(total.width + 2.0 * PAD);
size.height = size.height.max(total.height + header + 2.0 * PAD); size.height = size.height.max(total.height + header + 2.0 * PAD);
if order_boundaries {
boundary_groups.push(boundary::Group {
parent: id,
blocks: comps
.iter()
.enumerate()
.map(|(c, members)| boundary::Block {
members: members.iter().map(|&i| ids[i]).collect(),
size: blocks[c],
position: block_positions[c],
})
.collect(),
bands,
undirected: undirected
.iter()
.map(|(a, b)| (owner[index[a]], owner[index[b]]))
.collect(),
});
}
} }
let lifted: Vec<_> = arcs let lifted: Vec<_> = arcs
.iter() .iter()
@@ -472,6 +517,27 @@ pub fn candidate_with_diagnostics(
.collect(); .collect();
diagnostics.push(serde_json::json!({"visible_parent":parent,"components":component_diagnostics,"directed_lifted_edges":lifted,"undirected_lifted_pairs":undirected,"block_positions":block_positions,"bounds":total,"policy":"SCCs of sibling-subtree quotient, not global semantic SCCs. Undirected edges affect local gap and deterministic proximity only; no rank direction invented."})); diagnostics.push(serde_json::json!({"visible_parent":parent,"components":component_diagnostics,"directed_lifted_edges":lifted,"undirected_lifted_pairs":undirected,"block_positions":block_positions,"bounds":total,"policy":"SCCs of sibling-subtree quotient, not global semantic SCCs. Undirected edges affect local gap and deterministic proximity only; no rank direction invented."}));
} }
let boundary_report = if order_boundaries {
let report = boundary::reorder(
&mut boundary_groups,
&parents,
&mut offsets,
&sizes,
&view.edges,
&mut work,
)?;
for group in &boundary_groups {
let diagnostic = diagnostics
.iter_mut()
.find(|entry| entry["visible_parent"] == serde_json::json!(group.parent))
.unwrap();
diagnostic["block_positions"] =
serde_json::json!(group.blocks.iter().map(|b| b.position).collect::<Vec<_>>());
}
Some(report)
} else {
None
};
let mut plans = Vec::new(); let mut plans = Vec::new();
for n in &view.nodes { for n in &view.nodes {
let mut position = offsets[&n.id]; let mut position = offsets[&n.id];
@@ -518,10 +584,11 @@ pub fn candidate_with_diagnostics(
"final mandatory port lead incompatible; no repair attempted", "final mandatory port lead incompatible; no repair attempted",
)); ));
} }
Ok(( let mut report = serde_json::json!({"groups":diagnostics,"work_charged":work,"work_limit":LIMIT,"scope":"Projected-forest quotient heuristic; actual final reprojection and necessary lead guard checked. No full-route or readability acceptance."});
result, if let Some(boundary_report) = boundary_report {
serde_json::json!({"groups":diagnostics,"work_charged":work,"work_limit":LIMIT,"scope":"Projected-forest quotient heuristic; actual final reprojection and necessary lead guard checked. No full-route or readability acceptance."}), report["boundary_order"] = boundary_report;
)) }
Ok((result, report))
} }
#[cfg(test)] #[cfg(test)]
@@ -0,0 +1,672 @@
//! One deterministic boundary-order pass; dependency cycles use frozen scores.
use super::{charge, components, error, lift};
use oa_core::{Id, ModelError, Position, Size, projection::ProjectedEdge};
use std::collections::{BTreeMap, BTreeSet};
pub(super) struct Block {
pub members: Vec<Id>,
pub size: Size,
pub position: Position,
}
pub(super) struct Band {
pub components: Vec<usize>,
pub gap: f32,
}
pub(super) struct Group {
pub parent: Id,
pub blocks: Vec<Block>,
pub bands: Vec<Band>,
pub undirected: BTreeSet<(usize, usize)>,
}
#[derive(Clone)]
struct Unit {
blocks: Vec<usize>,
sources: BTreeSet<Id>,
hints: Vec<(Id, Id, Id)>,
}
fn center(
id: Id,
parents: &BTreeMap<Id, Option<Id>>,
offsets: &BTreeMap<Id, Position>,
sizes: &BTreeMap<Id, Size>,
work: &mut usize,
) -> Result<f64, ModelError> {
let mut at = Some(id);
let mut seen = BTreeSet::new();
let mut y = 0.0_f32;
while let Some(node) = at {
charge(work, 1)?;
if !seen.insert(node) {
return Err(error("boundary cyclic ancestry"));
}
let pos = offsets
.get(&node)
.ok_or_else(|| error("boundary missing offset"))?;
if !pos.x.is_finite() || !pos.y.is_finite() {
return Err(error("boundary nonfinite offset"));
}
if seen.len() == 1 {
y = pos.y;
} else {
y += pos.y;
}
if !y.is_finite() {
return Err(error("boundary composed position overflow"));
}
at = *parents
.get(&node)
.ok_or_else(|| error("boundary missing parent"))?;
}
let size = sizes
.get(&id)
.ok_or_else(|| error("boundary missing source size"))?;
y += 40.0;
let center = f64::from(y) + f64::from(size.height) / 2.0;
if !center.is_finite() {
return Err(error("boundary nonfinite center"));
}
Ok(center)
}
fn translate(value: f32, delta: f32) -> Result<f32, ModelError> {
let next = value + delta;
if !next.is_finite() || ((f64::from(next) - f64::from(value)) - f64::from(delta)).abs() > 0.001
{
return Err(error("boundary member translation precision exceeded"));
}
Ok(next)
}
fn score(
unit: &Unit,
parents: &BTreeMap<Id, Option<Id>>,
offsets: &BTreeMap<Id, Position>,
sizes: &BTreeMap<Id, Size>,
work: &mut usize,
) -> Result<Option<f64>, ModelError> {
if unit.sources.is_empty() {
return Ok(None);
}
let mut total = 0.0;
for &source in &unit.sources {
total += center(source, parents, offsets, sizes, work)?;
}
let value = total / unit.sources.len() as f64;
if !value.is_finite() {
return Err(error("boundary nonfinite mean"));
}
Ok(Some(value))
}
fn ordered_units(scores: &[Option<f64>]) -> Vec<usize> {
let mut anchored: Vec<_> = (0..scores.len()).filter(|&i| scores[i].is_some()).collect();
anchored.sort_by(|&a, &b| {
scores[a]
.unwrap()
.total_cmp(&scores[b].unwrap())
.then(a.cmp(&b))
});
let mut next = anchored.into_iter();
(0..scores.len())
.map(|i| {
if scores[i].is_some() {
next.next().unwrap()
} else {
i
}
})
.collect()
}
fn packing(
blocks: &[Block],
before: &[usize],
after: &[usize],
gap: f32,
) -> Result<Vec<f32>, ModelError> {
const EPS: f64 = 0.001;
let origin = f64::from(blocks[before[0]].position.y);
let mut canonical = origin;
for &c in before {
if (f64::from(blocks[c].position.y) - canonical).abs() > EPS {
return Err(error("boundary non-prefix canonical band"));
}
canonical += f64::from(blocks[c].size.height) + f64::from(gap);
}
let mut next = origin;
let mut ys = Vec::new();
for &c in after {
let y = next as f32;
if !y.is_finite() || (f64::from(y) - next).abs() > EPS {
return Err(error("boundary prefix precision bound exceeded"));
}
ys.push(y);
next += f64::from(blocks[c].size.height) + f64::from(gap);
}
if (next - canonical).abs() > EPS {
return Err(error("boundary envelope changed"));
}
if before == after {
Ok(before.iter().map(|&c| blocks[c].position.y).collect())
} else {
Ok(ys)
}
}
pub(super) fn reorder(
groups: &mut [Group],
parents: &BTreeMap<Id, Option<Id>>,
offsets: &mut BTreeMap<Id, Position>,
sizes: &BTreeMap<Id, Size>,
edges: &[ProjectedEdge],
work: &mut usize,
) -> Result<serde_json::Value, ModelError> {
let mut group_order: Vec<_> = (0..groups.len()).collect();
group_order.sort_by_key(|&i| groups[i].parent);
let mut offset_owner = BTreeMap::new();
let mut parent_seen = BTreeSet::new();
let mut units: Vec<Vec<Vec<Unit>>> = vec![Vec::new(); groups.len()];
let mut active = BTreeSet::new();
for &g in &group_order {
let group = &groups[g];
if !parent_seen.insert(group.parent) || !parents.contains_key(&group.parent) {
return Err(error("boundary duplicate/missing group parent"));
}
let mut child_block = BTreeMap::new();
for (c, block) in group.blocks.iter().enumerate() {
if !block.size.width.is_finite()
|| !block.size.height.is_finite()
|| block.size.width <= 0.0
|| block.size.height <= 0.0
|| !block.position.x.is_finite()
|| !block.position.y.is_finite()
{
return Err(error("boundary invalid block geometry"));
}
for &id in &block.members {
if parents.get(&id) != Some(&Some(group.parent))
|| !offsets.contains_key(&id)
|| child_block.insert(id, c).is_some()
|| offset_owner.insert(id, g).is_some()
{
return Err(error("boundary invalid child membership"));
}
}
}
let mut covered = BTreeSet::new();
for band in &group.bands {
if !band.gap.is_finite() || band.gap < 0.0 || band.components.is_empty() {
return Err(error("boundary invalid band"));
}
let local: BTreeMap<_, _> = band
.components
.iter()
.enumerate()
.map(|(i, &c)| (c, i))
.collect();
for &c in &band.components {
if c >= group.blocks.len() || !covered.insert(c) {
return Err(error("boundary duplicate/unknown band block"));
}
}
let mut arcs = vec![BTreeSet::new(); band.components.len()];
for &(a, b) in &group.undirected {
charge(work, 1)?;
if let (Some(&a), Some(&b)) = (local.get(&a), local.get(&b)) {
arcs[a].insert(b);
arcs[b].insert(a);
}
}
let bundles = components(&arcs, work)?;
let mut band_units = Vec::new();
for bundle in bundles {
if bundle.windows(2).any(|v| v[1] != v[0] + 1) {
return Err(error("boundary undirected bundle not contiguous"));
}
let blocks: Vec<_> = bundle.iter().map(|&i| band.components[i]).collect();
let mut unit = Unit {
blocks,
sources: BTreeSet::new(),
hints: Vec::new(),
};
for edge in edges {
charge(work, 1)?;
if !edge.oriented {
continue;
}
let target = lift(edge.target, Some(group.parent), parents, work)?;
let Some(target) = target else {
continue;
};
if !unit.blocks.contains(
child_block
.get(&target)
.ok_or_else(|| error("boundary missing lifted target"))?,
) {
continue;
}
if lift(edge.source, Some(group.parent), parents, work)?.is_none() {
unit.sources.insert(edge.source);
unit.hints.push((edge.id, edge.source, edge.target));
}
}
unit.hints.sort();
band_units.push(unit);
}
if band_units.iter().filter(|u| !u.sources.is_empty()).count() >= 2 {
active.insert(g);
}
units[g].push(band_units);
}
if covered.len() != group.blocks.len() {
return Err(error("boundary incomplete band inventory"));
}
}
let active_order: Vec<_> = group_order
.into_iter()
.filter(|g| active.contains(g))
.collect();
let active_index: BTreeMap<_, _> = active_order
.iter()
.enumerate()
.map(|(i, &g)| (g, i))
.collect();
let mut dependencies = vec![BTreeSet::new(); active_order.len()];
let mut dependency_evidence = BTreeSet::new();
for (&g, &gi) in &active_index {
for unit in units[g].iter().flatten() {
for &source in &unit.sources {
let mut at = Some(source);
let mut seen = BTreeSet::new();
while let Some(node) = at {
charge(work, 1)?;
if !seen.insert(node) {
return Err(error("boundary dependency ancestry cycle"));
}
if let Some(&owner) = offset_owner.get(&node)
&& let Some(&oi) = active_index.get(&owner)
{
dependencies[oi].insert(gi);
dependency_evidence.insert((
groups[owner].parent,
groups[g].parent,
source,
node,
));
}
at = *parents
.get(&node)
.ok_or_else(|| error("boundary missing dependency ancestor"))?;
}
}
}
}
let sccs = components(&dependencies, work)?;
let mut scc_owner = vec![0; active_order.len()];
for (s, members) in sccs.iter().enumerate() {
for &i in members {
scc_owner[i] = s;
}
}
let mut dag = vec![BTreeSet::new(); sccs.len()];
for (a, targets) in dependencies.iter().enumerate() {
for &b in targets {
if scc_owner[a] != scc_owner[b] {
dag[scc_owner[a]].insert(scc_owner[b]);
}
}
}
let mut done = BTreeSet::new();
let mut records = Vec::new();
let mut processed = Vec::new();
while done.len() < sccs.len() {
charge(work, sccs.len() * sccs.len())?;
let s = (0..sccs.len())
.find(|s| {
!done.contains(s)
&& (0..sccs.len()).all(|p| !dag[p].contains(s) || done.contains(&p))
})
.ok_or_else(|| error("boundary condensation cycle"))?;
// Score the entire dependency SCC before modifying any offsets.
let mut snapshots = Vec::new();
for &i in &sccs[s] {
let g = active_order[i];
for (b, band_units) in units[g].iter().enumerate() {
let scores = band_units
.iter()
.map(|u| score(u, parents, offsets, sizes, work))
.collect::<Result<Vec<_>, _>>()?;
snapshots.push((g, b, scores));
}
}
for (g, b, scores) in snapshots {
let band_units = &units[g][b];
let order = ordered_units(&scores);
let before = groups[g].bands[b].components.clone();
let gap = groups[g].bands[b].gap;
let after: Vec<_> = order
.iter()
.flat_map(|&u| band_units[u].blocks.iter().copied())
.collect();
let ys = packing(&groups[g].blocks, &before, &after, gap)?;
let mut changed = Vec::new();
for (&c, y) in after.iter().zip(ys) {
charge(work, groups[g].blocks[c].members.len() + 1)?;
let old = groups[g].blocks[c].position.y;
let delta = y - old;
if !y.is_finite() || !delta.is_finite() {
return Err(error("boundary packing overflow"));
}
if delta != 0.0 {
changed.push(c);
}
for id in &groups[g].blocks[c].members {
let p = offsets
.get_mut(id)
.ok_or_else(|| error("boundary lost member offset"))?;
p.y = translate(p.y, delta)?;
}
groups[g].blocks[c].position.y = y;
}
groups[g].bands[b].components = after.clone();
let changed_units: Vec<_> = band_units
.iter()
.enumerate()
.filter(|(_, u)| u.blocks.iter().any(|c| changed.contains(c)))
.map(|(i, _)| i)
.collect();
records.push(serde_json::json!({"parent":groups[g].parent,"band":b,"before_blocks":before,"after_blocks":after,"unit_order":order,"scores":scores,"changed_blocks":changed,"changed_units":changed_units,"envelope_epsilon":0.001,"units":band_units.iter().map(|u|serde_json::json!({"blocks":u.blocks,"source_owners":u.sources,"edge_hints":u.hints})).collect::<Vec<_>>()}));
}
processed.push(serde_json::json!({"parents":sccs[s].iter().map(|&i|groups[active_order[i]].parent).collect::<Vec<_>>(),"cyclic":sccs[s].len()>1||sccs[s].iter().any(|&i|dependencies[i].contains(&i)),"policy":"all scores snapshotted before one simultaneous group-permutation pass"}));
done.insert(s);
}
Ok(
serde_json::json!({"active_parents":active_order.iter().map(|&g|groups[g].parent).collect::<Vec<_>>(),"dependencies":dependency_evidence,"dependency_components":processed,"bands":records,"limitation":"One pass; cyclic dependencies use a common snapshot, not a fixed-point solution. Root unchanged; no quality or repeat-stability acceptance."}),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn id(n: u128) -> Id {
Id::from_u128(n)
}
fn block(n: u128, y: f32, height: f32) -> Block {
Block {
members: vec![id(n)],
size: Size {
width: 10.0,
height,
},
position: Position { x: 0.0, y },
}
}
fn edge(n: u128, source: u128, target: u128) -> ProjectedEdge {
ProjectedEdge {
id: id(n),
source: id(source),
target: id(target),
trace: false,
oriented: true,
source_port: None,
target_port: None,
}
}
#[test]
fn anchored_sort_preserves_unanchored_ordinal_slot() {
assert_eq!(
ordered_units(&[Some(90.0), None, Some(10.0), Some(10.0)]),
vec![2, 1, 3, 0]
);
}
#[test]
fn heterogeneous_blocks_use_new_prefix_not_old_slots() {
let blocks = vec![
Block {
members: vec![],
size: Size {
width: 10.0,
height: 100.0,
},
position: Position { x: 0.0, y: 0.0 },
},
Block {
members: vec![],
size: Size {
width: 10.0,
height: 20.0,
},
position: Position { x: 0.0, y: 132.0 },
},
];
assert_eq!(
packing(&blocks, &[0, 1], &[1, 0], 32.0).unwrap(),
vec![0.0, 52.0]
);
}
#[test]
fn center_includes_grandparent_offset_change() {
let (a, b, c) = (Id::from_u128(1), Id::from_u128(2), Id::from_u128(3));
let parents = BTreeMap::from([(a, None), (b, Some(a)), (c, Some(b))]);
let mut offsets = BTreeMap::from([
(a, Position { x: 0.0, y: 100.0 }),
(b, Position { x: 0.0, y: 20.0 }),
(c, Position { x: 0.0, y: 5.0 }),
]);
let sizes = BTreeMap::from([(
c,
Size {
width: 10.0,
height: 10.0,
},
)]);
assert_eq!(
center(c, &parents, &offsets, &sizes, &mut 0).unwrap(),
170.0
);
offsets.get_mut(&a).unwrap().y += 300.0;
assert_eq!(
center(c, &parents, &offsets, &sizes, &mut 0).unwrap(),
470.0
);
}
#[test]
fn ancestor_dependency_updates_downstream_scores_before_its_permutation() {
// Downstream parent1 sorts before upstream20, so an absent ancestor
// dependency would leave downstream order unchanged and fail this test.
let mut groups = vec![
Group {
parent: id(1),
blocks: vec![block(2, 0.0, 100.0), block(3, 132.0, 100.0)],
bands: vec![Band {
components: vec![0, 1],
gap: 32.0,
}],
undirected: BTreeSet::new(),
},
Group {
parent: id(20),
blocks: vec![block(21, 0.0, 100.0), block(22, 132.0, 100.0)],
bands: vec![Band {
components: vec![0, 1],
gap: 32.0,
}],
undirected: BTreeSet::new(),
},
];
let parents = BTreeMap::from([
(id(1), None),
(id(2), Some(id(1))),
(id(3), Some(id(1))),
(id(20), None),
(id(21), Some(id(20))),
(id(22), Some(id(20))),
(id(23), Some(id(21))),
(id(30), None),
(id(31), None),
(id(32), None),
]);
let mut offsets: BTreeMap<_, _> = [
(1, 0.0),
(2, 0.0),
(3, 132.0),
(20, 0.0),
(21, 0.0),
(22, 132.0),
(23, 10.0),
(30, 300.0),
(31, 0.0),
(32, 50.0),
]
.into_iter()
.map(|(n, y)| (id(n), Position { x: 0.0, y }))
.collect();
let sizes = parents
.keys()
.map(|&n| {
(
n,
Size {
width: 10.0,
height: 10.0,
},
)
})
.collect();
let edges = vec![
edge(101, 30, 21),
edge(102, 31, 22),
edge(103, 23, 2),
edge(104, 32, 3),
];
let report = reorder(&mut groups, &parents, &mut offsets, &sizes, &edges, &mut 0).unwrap();
assert_eq!(groups[1].bands[0].components, vec![1, 0]);
assert_eq!(groups[0].bands[0].components, vec![1, 0]);
assert_eq!(offsets[&id(21)].y, 132.0);
assert_eq!(offsets[&id(23)].y, 10.0); // descendant is not directly moved
assert_eq!(
center(id(23), &parents, &offsets, &sizes, &mut 0).unwrap(),
187.0
);
assert_eq!(
report["dependency_components"][0]["parents"][0],
serde_json::json!(id(20))
);
}
#[test]
fn undirected_bundle_stays_whole_around_unanchored_unit() {
let mut groups = vec![Group {
parent: id(100),
blocks: vec![
block(101, 0.0, 20.0),
block(102, 30.0, 30.0),
block(103, 70.0, 40.0),
block(104, 120.0, 10.0),
],
bands: vec![Band {
components: vec![0, 1, 2, 3],
gap: 10.0,
}],
undirected: BTreeSet::from([(0, 1)]),
}];
let parents = BTreeMap::from([
(id(100), None),
(id(101), Some(id(100))),
(id(102), Some(id(100))),
(id(103), Some(id(100))),
(id(104), Some(id(100))),
(id(110), None),
(id(111), None),
]);
let mut offsets: BTreeMap<_, _> = [
(100, 0.0),
(101, 0.0),
(102, 30.0),
(103, 70.0),
(104, 120.0),
(110, 300.0),
(111, 0.0),
]
.into_iter()
.map(|(n, y)| (id(n), Position { x: 0.0, y }))
.collect();
let sizes = parents
.keys()
.map(|&n| {
(
n,
Size {
width: 10.0,
height: 10.0,
},
)
})
.collect();
let report = reorder(
&mut groups,
&parents,
&mut offsets,
&sizes,
&[
edge(201, 110, 101),
edge(202, 110, 102),
edge(203, 111, 104),
],
&mut 0,
)
.unwrap();
assert_eq!(groups[0].bands[0].components, vec![3, 2, 0, 1]);
assert_eq!(
[
offsets[&id(104)].y,
offsets[&id(103)].y,
offsets[&id(101)].y,
offsets[&id(102)].y
],
[0.0, 20.0, 70.0, 100.0]
);
assert_eq!(
report["bands"][0]["units"][0]["source_owners"]
.as_array()
.unwrap()
.len(),
1
);
}
#[test]
fn center_uses_final_plan_f32_composition_and_translation_has_error_bound() {
let parents = BTreeMap::from([(id(1), None), (id(2), Some(id(1))), (id(3), Some(id(2)))]);
let offsets = BTreeMap::from([
(
id(1),
Position {
x: 0.0,
y: -100000.0,
},
),
(
id(2),
Position {
x: 0.0,
y: 100000.0,
},
),
(id(3), Position { x: 0.0, y: 0.1 }),
]);
let sizes = BTreeMap::from([(
id(3),
Size {
width: 10.0,
height: 10.0,
},
)]);
assert_eq!(
center(id(3), &parents, &offsets, &sizes, &mut 0).unwrap(),
45.1015625
);
assert_eq!(translate(10.125, 0.375).unwrap(), 10.5);
assert!(translate(32768.0, 0.002).is_err());
}
}