pub use nucleation_routing::{self as engine, *};
use crate::UniversalSchematic;
pub fn workspace_from_schematic(schem: &UniversalSchematic) -> Workspace {
Workspace::from_blocks(schem.iter_blocks().filter_map(|(bp, bs)| {
if bs.name.as_str() == "minecraft:air" {
return None;
}
Some((Pos::new(bp.x, bp.y, bp.z), bs.to_string()))
}))
}
pub fn apply_workspace(schem: &mut UniversalSchematic, ws: &Workspace) -> Result<usize, String> {
let mut n = 0;
for (p, block) in ws.cells() {
schem.set_block_from_string(p.x, p.y, p.z, block)?;
n += 1;
}
Ok(n)
}
pub fn route_net(
schem: &mut UniversalSchematic,
src: (i32, i32, i32),
dst: (i32, i32, i32),
label: &str,
) -> Result<RouteResult, String> {
let mut ws = workspace_from_schematic(schem);
let router = RedstoneRouter::new();
let res = router
.route(
&mut ws,
Pos::new(src.0, src.1, src.2),
Pos::new(dst.0, dst.1, dst.2),
label,
&[],
)
.map_err(|e| format!("{e:?}"))?;
apply_workspace(schem, &ws)?;
Ok(res)
}
pub fn drc_schematic(schem: &UniversalSchematic, opts: &DrcOptions) -> Vec<Violation> {
let ws = workspace_from_schematic(schem);
drc(&ws, opts)
}
pub fn violations_json(vs: &[Violation]) -> String {
let items: Vec<String> = vs
.iter()
.map(|v| match v {
Violation::Short {
label_a,
label_b,
at_a,
at_b,
} => format!(
"{{\"kind\":\"short\",\"label_a\":{label_a:?},\"label_b\":{label_b:?},\"at_a\":[{},{},{}],\"at_b\":[{},{},{}]}}",
at_a.x, at_a.y, at_a.z, at_b.x, at_b.y, at_b.z
),
Violation::Floating { at, block } => format!(
"{{\"kind\":\"floating\",\"at\":[{},{},{}],\"block\":{block:?}}}",
at.x, at.y, at.z
),
Violation::UnattachedWallTorch { at, anchor } => format!(
"{{\"kind\":\"unattached_wall_torch\",\"at\":[{},{},{}],\"anchor\":[{},{},{}]}}",
at.x, at.y, at.z, anchor.x, anchor.y, anchor.z
),
Violation::RepeaterCycle { diodes } => {
let ds: Vec<String> = diodes
.iter()
.map(|p| format!("[{},{},{}]", p.x, p.y, p.z))
.collect();
format!("{{\"kind\":\"repeater_cycle\",\"diodes\":[{}]}}", ds.join(","))
}
Violation::PowerStarved { at, distance } => format!(
"{{\"kind\":\"power_starved\",\"at\":[{},{},{}],\"distance\":{distance}}}",
at.x, at.y, at.z
),
})
.collect();
format!("[{}]", items.join(","))
}
fn intersect_aabb(a: Aabb, b: Aabb) -> Result<Aabb, String> {
let min = Pos::new(
a.min.x.max(b.min.x),
a.min.y.max(b.min.y),
a.min.z.max(b.min.z),
);
let max = Pos::new(
a.max.x.min(b.max.x),
a.max.y.min(b.max.y),
a.max.z.min(b.max.z),
);
if min.x > max.x || min.y > max.y || min.z > max.z {
return Err("bounds and y_band do not overlap".to_string());
}
Ok(Aabb { min, max })
}
fn parse_pos(v: &serde_json::Value, what: &str) -> Result<Pos, String> {
let c = v
.as_array()
.filter(|c| c.len() == 3)
.ok_or_else(|| format!("{what} must be [x, y, z]"))?;
let g = |i: usize| -> Result<i32, String> {
c[i].as_i64()
.map(|v| v as i32)
.ok_or_else(|| format!("{what} coordinate must be an integer"))
};
Ok(Pos::new(g(0)?, g(1)?, g(2)?))
}
pub fn route_all_schematic(
schem: &mut UniversalSchematic,
nets_json: &str,
) -> Result<String, String> {
use crate::io_contract::routing::{NetClassRule, RoutingRegion as ContractRegion};
use serde_json::json;
use std::collections::HashMap;
let parsed: serde_json::Value =
serde_json::from_str(nets_json).map_err(|e| format!("route_all JSON: {e}"))?;
struct NetIn {
label: String,
src: Pos,
dsts: Vec<Pos>,
friendly: Vec<String>,
class: Option<String>,
}
let nets_in: Vec<NetIn> = parsed["nets"]
.as_array()
.ok_or("route_all needs a `nets` array")?
.iter()
.map(|n| {
let label = n["label"]
.as_str()
.ok_or("net needs a `label`")?
.to_string();
let src = parse_pos(&n["src"], "src")?;
let dsts = n["dsts"]
.as_array()
.ok_or("net needs a `dsts` array")?
.iter()
.map(|d| parse_pos(d, "dst"))
.collect::<Result<Vec<Pos>, String>>()?;
if dsts.is_empty() {
return Err(format!("net `{label}` has no destinations"));
}
let friendly = n["friendly"]
.as_array()
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default();
let class = n["class"].as_str().map(str::to_string);
Ok(NetIn {
label,
src,
dsts,
friendly,
class,
})
})
.collect::<Result<_, String>>()?;
let classes: HashMap<String, NetClassRule> = match &parsed["classes"] {
serde_json::Value::Null => HashMap::new(),
v => serde_json::from_value(v.clone()).map_err(|e| format!("classes: {e}"))?,
};
let zones = ContractRegion::collect(schem.definition_regions.values());
let to_engine_region = |zone: &ContractRegion| RoutingRegion {
include: zone
.include
.iter()
.map(|b| {
Aabb::new(
Pos::new(b.min.0, b.min.1, b.min.2),
Pos::new(b.max.0, b.max.1, b.max.2),
)
})
.collect(),
exclude: zone
.exclude
.iter()
.map(|b| {
Aabb::new(
Pos::new(b.min.0, b.min.1, b.min.2),
Pos::new(b.max.0, b.max.1, b.max.2),
)
})
.collect(),
};
let mut base = RedstoneRouter::new();
if let Some(b) = parsed.get("bounds").filter(|v| !v.is_null()) {
let arr = b
.as_array()
.filter(|a| a.len() == 2)
.ok_or("bounds must be [[x,y,z],[x,y,z]]")?;
base.bounds = Some(Aabb::new(
parse_pos(&arr[0], "bounds min")?,
parse_pos(&arr[1], "bounds max")?,
));
}
if let Some(b) = parsed.get("budget").filter(|v| !v.is_null()) {
if let Some(r) = b["refresh"].as_u64() {
base.budget.refresh = r as u32;
}
if let Some(s) = b["stair_cap"].as_u64() {
base.budget.stair_cap = s as u8;
}
}
if let Some(c) = parsed.get("congestion").filter(|v| !v.is_null()) {
if let Some(r) = c["max_rounds"].as_u64() {
base.congestion.max_rounds = r as usize;
}
if let Some(h) = c["history_increment"].as_u64() {
base.congestion.history_increment = h as u32;
}
if let Some(p) = c["present_penalty"].as_u64() {
base.congestion.present_penalty = p as u32;
}
}
let mut order: Vec<Option<String>> = Vec::new();
let mut groups: HashMap<Option<String>, Vec<usize>> = HashMap::new();
for (i, n) in nets_in.iter().enumerate() {
let key = n.class.clone();
if !groups.contains_key(&key) {
order.push(key.clone());
}
groups.entry(key).or_default().push(i);
}
let mut ws = workspace_from_schematic(schem);
let mut notes: Vec<String> = Vec::new();
let mut routed: Vec<Option<(RouteResult, Option<String>)>> = vec![None; nets_in.len()];
for key in &order {
let members = &groups[key];
let mut router = base.clone();
if let Some(class_name) = key {
let rule = classes.get(class_name).ok_or_else(|| {
format!("net class `{class_name}` is not defined under `classes`")
})?;
if let Some(region_name) = &rule.region {
let zone = zones.get(region_name).ok_or_else(|| {
format!(
"routing region `{region_name}` is not tagged on any of the \
schematic's DefinitionRegions"
)
})?;
router.region = Some(to_engine_region(zone));
}
if let Some((y0, y1)) = rule.y_band {
const FAR: i32 = 1 << 24;
let band = Aabb::new(Pos::new(-FAR, y0, -FAR), Pos::new(FAR, y1, FAR));
router.bounds = Some(match router.bounds {
Some(b) => {
intersect_aabb(b, band).map_err(|e| format!("class `{class_name}`: {e}"))?
}
None => band,
});
}
if rule.spacing != 0 {
notes.push(format!(
"class `{class_name}`: `spacing` is not enforced by route_all v1"
));
}
if rule.direction_bias.is_some() {
notes.push(format!(
"class `{class_name}`: `direction_bias` is not enforced by route_all v1"
));
}
}
let group_nets: Vec<NetRoute> = members
.iter()
.map(|&i| NetRoute {
src: nets_in[i].src,
dsts: nets_in[i].dsts.clone(),
label: nets_in[i].label.clone(),
friendly: nets_in[i].friendly.clone(),
})
.collect();
let results = router
.route_all(&mut ws, &group_nets)
.map_err(|e| match e {
RouteError::Congestion {
unrouted,
contested,
} => format!(
"congestion did not converge: unrouted {unrouted:?}, contested {} cells",
contested.len()
),
other => format!("{other:?}"),
})?;
for (&i, res) in members.iter().zip(results) {
routed[i] = Some((res, key.clone()));
}
}
apply_workspace(schem, &ws)?;
let mut violations: Vec<serde_json::Value> = Vec::new();
let routes: Vec<serde_json::Value> = nets_in
.iter()
.zip(&routed)
.map(|(n, slot)| {
let (res, class) = slot.as_ref().expect("every net routed");
let delay_rt: u32 = res
.path
.iter()
.filter_map(|p| ws.get(*p))
.filter(|b| engine::blocks::is_repeater(b))
.map(engine::blocks::repeater_delay)
.sum();
if let Some(max) = class
.as_ref()
.and_then(|c| classes.get(c))
.and_then(|r| r.max_len_rt)
{
if delay_rt > max {
violations.push(serde_json::json!({
"kind": "max_len_rt",
"label": n.label,
"delay_rt": delay_rt,
"max_len_rt": max,
}));
}
}
serde_json::json!({
"label": n.label,
"class": class,
"cells": res.cells,
"delay_rt": delay_rt,
"path": res.path.iter().map(|p| serde_json::json!([p.x, p.y, p.z])).collect::<Vec<_>>(),
})
})
.collect();
Ok(json!({
"routes": routes,
"notes": notes,
"violations": violations,
})
.to_string())
}
pub fn parse_intent_nets(json: &str) -> Result<Vec<IntentNet>, String> {
let parsed: serde_json::Value =
serde_json::from_str(json).map_err(|e| format!("intent netlist JSON: {e}"))?;
let nets = parsed["nets"]
.as_array()
.ok_or("intent netlist needs a `nets` array")?;
nets.iter()
.map(|n| {
let name = n["name"].as_str().ok_or("net needs a `name`")?.to_string();
let terminals = n["terminals"]
.as_array()
.ok_or("net needs a `terminals` array")?
.iter()
.map(|t| {
let c = t
.as_array()
.filter(|c| c.len() == 3)
.ok_or("terminal must be [x, y, z]")?;
let g = |i: usize| -> Result<i32, String> {
c[i].as_i64()
.map(|v| v as i32)
.ok_or_else(|| "terminal coordinate must be an integer".to_string())
};
Ok(Pos::new(g(0)?, g(1)?, g(2)?))
})
.collect::<Result<Vec<Pos>, String>>()?;
Ok(IntentNet { name, terminals })
})
.collect()
}
pub fn lvs_schematic(schem: &UniversalSchematic, intent_json: &str) -> Result<LvsReport, String> {
let intent = parse_intent_nets(intent_json)?;
let ws = workspace_from_schematic(schem);
Ok(lvs(ws.cells(), &intent))
}
pub fn lvs_report_json(report: &LvsReport) -> String {
use serde_json::json;
let pos = |p: &Pos| json!([p.x, p.y, p.z]);
json!({
"clean": report.clean(),
"matched": report.matched,
"opens": report.opens.iter().map(|o| json!({
"net": o.net,
"fragments": o.fragments.iter()
.map(|f| f.iter().map(pos).collect::<Vec<_>>())
.collect::<Vec<_>>(),
})).collect::<Vec<_>>(),
"shorts": report.shorts.iter().map(|s| json!({
"net_a": s.net_a,
"net_b": s.net_b,
"at_a": pos(&s.at_a),
"at_b": pos(&s.at_b),
})).collect::<Vec<_>>(),
"cycles": report.cycles.iter()
.map(|c| c.iter().map(pos).collect::<Vec<_>>())
.collect::<Vec<_>>(),
})
.to_string()
}
pub fn sta_schematic(
schem: &UniversalSchematic,
inputs: &[String],
gates: &[sta::Gate],
) -> Result<sta::TimingReport, sta::StaError> {
let ws = workspace_from_schematic(schem);
sta::sta(&ws, inputs, gates)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_and_route_over_schematic() {
let mut schem = UniversalSchematic::new("routing_test".to_string());
for z in -1..=1 {
schem
.set_block_from_string(3, 1, z, "minecraft:stone")
.unwrap();
}
let res = route_net(&mut schem, (0, 1, 0), (6, 1, 0), "n").unwrap();
assert_eq!(res.path.first().copied(), Some(Pos::new(0, 1, 0)));
assert_eq!(res.path.last().copied(), Some(Pos::new(6, 1, 0)));
let end = schem.get_block(6, 1, 0).expect("dst block");
assert!(end.name.contains("redstone_wire"));
assert!(end.to_string().contains("power=0"), "full state expected");
let vs = drc_schematic(
&schem,
&DrcOptions {
aliases: vec![],
skip_decay: true,
},
);
assert!(vs.is_empty(), "{vs:?}");
}
#[test]
fn lvs_matches_a_routed_net_and_catches_a_break() {
let mut schem = UniversalSchematic::new("lvs_test".to_string());
let res = route_net(&mut schem, (0, 1, 0), (6, 1, 0), "n").unwrap();
let intent = r#"{"nets": [{"name": "n", "terminals": [[0, 1, 0], [6, 1, 0]]}]}"#;
let r = lvs_schematic(&schem, intent).unwrap();
assert_eq!(r.matched, vec!["n".to_string()], "{r:?}");
assert!(r.clean(), "{r:?}");
let mid = res.path[res.path.len() / 2];
schem
.set_block_from_string(mid.x, mid.y, mid.z, "minecraft:air")
.unwrap();
let r = lvs_schematic(&schem, intent).unwrap();
assert_eq!(r.opens.len(), 1, "{r:?}");
assert_eq!(r.opens[0].net, "n");
let json = lvs_report_json(&r);
assert!(json.contains("\"clean\":false"), "{json}");
}
#[test]
fn route_all_negotiates_two_nets_through_separate_windows() {
let mut schem = UniversalSchematic::new("windows".to_string());
for z in -2..=6 {
for y in 1..=2 {
if z == 0 || z == 5 {
continue;
}
schem
.set_block_from_string(3, y, z, "minecraft:stone")
.unwrap();
}
}
let req = r#"{
"nets": [
{"label": "a", "src": [0, 1, 0], "dsts": [[6, 1, 0]]},
{"label": "b", "src": [0, 1, 2], "dsts": [[6, 1, 2]]}
],
"bounds": [[-1, 1, -2], [8, 2, 6]]
}"#;
let report = route_all_schematic(&mut schem, req).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&report).unwrap();
let routes = parsed["routes"].as_array().unwrap();
assert_eq!(routes.len(), 2);
let crossing = |route: &serde_json::Value| -> Vec<i64> {
route["path"]
.as_array()
.unwrap()
.iter()
.filter(|p| p[0].as_i64() == Some(3))
.map(|p| p[2].as_i64().unwrap())
.collect()
};
let za = crossing(&routes[0]);
let zb = crossing(&routes[1]);
assert!(!za.is_empty() && !zb.is_empty(), "{report}");
for z in za.iter().chain(&zb) {
assert!(*z == 0 || *z == 5, "crossed outside a window: {report}");
}
assert!(
za.iter().all(|z| zb.iter().all(|w| w != z)),
"both nets took the same window: {report}"
);
let intent = r#"{"nets": [
{"name": "a", "terminals": [[0, 1, 0], [6, 1, 0]]},
{"name": "b", "terminals": [[0, 1, 2], [6, 1, 2]]}
]}"#;
let lvs = lvs_schematic(&schem, intent).unwrap();
assert!(lvs.clean(), "{lvs:?}");
assert_eq!(lvs.matched.len(), 2, "{lvs:?}");
}
#[test]
fn route_all_honours_class_region_and_y_band() {
use crate::io_contract::routing::{RouteZoneMode, RoutingRegion as ContractRegion};
let mut schem = UniversalSchematic::new("classes".to_string());
for z in -1..=1 {
for y in 1..=2 {
schem
.set_block_from_string(3, y, z, "minecraft:stone")
.unwrap();
}
}
let mut dr = crate::definition_region::DefinitionRegion::from_bounds((-2, 0, 2), (9, 3, 9));
ContractRegion::tag(&mut dr, "north_only", RouteZoneMode::Exclude);
schem
.definition_regions
.insert("north_only".to_string(), dr);
let req = r#"{
"nets": [{"label": "n", "src": [0, 1, 0], "dsts": [[6, 1, 0]],
"class": "c"}],
"classes": {"c": {"region": "north_only", "y_band": [1, 1],
"max_len_rt": 8}},
"bounds": [[-1, 0, -6], [8, 2, 6]]
}"#;
let report = route_all_schematic(&mut schem, req).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&report).unwrap();
let path = parsed["routes"][0]["path"].as_array().unwrap();
for p in path {
let (y, z) = (p[1].as_i64().unwrap(), p[2].as_i64().unwrap());
assert_eq!(y, 1, "y_band violated: {report}");
assert!(z < 2, "excluded region entered: {report}");
}
assert!(
parsed["violations"].as_array().unwrap().is_empty(),
"{report}"
);
let bad = r#"{
"nets": [{"label": "n2", "src": [0, 1, -4], "dsts": [[6, 1, -4]],
"class": "c"}],
"classes": {"c": {"region": "nope"}}
}"#;
let err = route_all_schematic(&mut schem, bad).unwrap_err();
assert!(err.contains("nope"), "{err}");
}
}