main
John Lauer 0.9.6: inputs a solve assumed (datasheet fields, spec patch), reflections and TDR 4271eae
//! The inputs a solve assumed (0.9.6): every default or estimate a result leans on, with the part and its MPN, the
//! datasheet field that replaces it, the spec key it goes in, and what moves if it is wrong (a sensitivity where
//! one can be computed). Sorted by how much it matters, so the caller's AI reads the datasheets that change the
//! answer first. `specPatch` is the spec fragment to fill in, with each value a placeholder naming its source.
use crate::board::Board;
use crate::fields::Spec;
use serde_json::{json, Value};
use std::f64::consts::PI;

fn r1(v: f64) -> f64 { (v * 10.0).round() / 10.0 }
fn has(raw: &Value, path: &[&str]) -> bool { let mut c = raw; for k in path { c = &c[*k]; } !c.is_null() }
fn set(patch: &mut Value, path: &[String], v: Value) { let mut c = patch; for k in &path[..path.len() - 1] { if !c[k.as_str()].is_object() { c[k.as_str()] = json!({}); } c = c.get_mut(k.as_str()).unwrap(); } c[path[path.len() - 1].as_str()] = v; }

pub fn review(b: &Board, spec: &Spec, raw: &Value, rev: &Value, chips: &Value) -> Value {
    let mut items: Vec<(u8, Value)> = Vec::new(); // (rank: 0 high, 1 medium, 2 low)
    let mut patch = json!({});
    let mpn = |r: &str| b.mpns.get(r).cloned().or_else(|| b.values.get(r).cloned()).unwrap_or_default();
    let mut item = |rank: u8, what: String, used: Value, from: &str, part: Option<&str>, field: &str, key: Vec<String>, affects: String, patch_v: Option<String>| {
        let p = part.map(|r| json!({"ref": r, "mpn": mpn(r)}));
        if let Some(v) = patch_v { set(&mut patch, &key, json!(format!("<{v}>"))); }
        items.push((rank, json!({"what": what, "used": used, "from": from, "part": p, "datasheetField": field, "specKey": key.join("."), "affects": affects})));
    };
    let c = &rev["converter"];
    // ---- the switching converter: frequency and edge set every emission number
    if let (Some(sw), Some(swr)) = (c["switchNet"].as_str(), c["switch"].as_str()) {
        let worst = rev["compliance"]["sources"].as_array().and_then(|s| s.iter().filter(|x| x["test"] == "radiated").min_by(|a, z| a["marginDb"].as_f64().partial_cmp(&z["marginDb"].as_f64()).unwrap()).cloned());
        if !has(raw, &["switching", sw, "riseNs"]) {
            let tr = c["riseNs"].as_f64().unwrap_or(8.0) * 1e-9;
            let sens = worst.as_ref().and_then(|w| w["fMHz"].as_f64()).map(|f| { let f = f * 1e6; let e = |t: f64| (1.0 / (PI * f * t)).min(1.0); (r1(20.0 * (e(tr / 2.0) / e(tr)).log10()), r1(20.0 * (e(tr) / e(tr * 2.0)).log10())) });
            item(if worst.as_ref().map_or(false, |w| w["marginDb"].as_f64().unwrap_or(99.0) < 12.0) { 0 } else { 1 }, format!("the switch edge on {sw}"), json!(format!("{} ns (default)", tr * 1e9)), "default", Some(swr), "switch-node rise and fall time (or the slew rate; the switching waveform figure)", vec!["switching".into(), sw.into(), "riseNs".into()],
                match (&worst, sens) { (Some(w), Some((up, dn))) => format!("every radiated margin above {:.0} MHz: half the edge time costs {up} dB on {}, twice it gains {dn} dB", 1e3 / (PI * tr * 1e9), w["id"].as_str().unwrap_or("")), _ => "the emission harmonics above 1/(π·tr)".into() }, Some(format!("{} datasheet: switch-node rise time, ns", mpn(swr))));
        }
        if !has(raw, &["switching", sw, "freqHz"]) {
            item(0, format!("the switching frequency on {sw}"), json!(format!("{} kHz (default)", c["fswHz"].as_f64().unwrap_or(500e3) / 1e3)), "default", Some(swr), "switching frequency (or the RT resistor's equation and its value on this board)", vec!["switching".into(), sw.into(), "freqHz".into()], "ripple, the hot loop's harmonics, conducted emissions and the input filter: every converter number".into(), Some(format!("{} datasheet: fSW, Hz", mpn(swr))));
        }
        for cap in c["inCaps"].as_array().into_iter().chain(c["outCaps"].as_array()).flatten() {
            let bm = cap["biasModel"].as_str().unwrap_or("");
            let r = cap["ref"].as_str().unwrap_or("");
            if bm.starts_with("estimate") { item(1, format!("{r}'s capacitance at its DC bias"), json!(format!("{} µF of {} µF ({bm})", cap["uFAtBias"], cap["uF"])), "estimate", Some(r), "capacitance vs DC bias curve (the vendor's simulator: SimSurfing, K-SIM, KSIM3)", vec!["capCurves".into()], "converter ripple, the hot loop's charge, PDN impedance and conducted emissions; class II ceramics vary ±30 % between vendors at the same bias".into(), None); }
            if cap["type"] == "electrolytic" || cap["type"] == "polymer" { item(1, format!("{r}'s ESR"), json!(format!("{} mΩ (typical for the type)", cap["esrMohm"])), "estimate", Some(r), "ESR at 100 kHz (the ripple-current table)", vec!["capCurves".into()], "ripple, damping of the input filter and the PDN's low-frequency impedance".into(), None); }
        }
    }
    // ---- heat: junction limits and θJC
    for j in rev["checks"]["junctions"].as_array().into_iter().flatten() {
        let r = j["reference"].as_str().unwrap_or("");
        // a passive (inductor, resistor, capacitor) has a maximum operating temperature, no junction and no θJC
        let passive = r.starts_with('L') || r.starts_with('R') || r.starts_with('C') || r.starts_with("FB");
        if passive { if !has(raw, &["hot", r, "tjMaxC"]) { item(if j["marginC"].as_f64().unwrap_or(99.0) < 25.0 { 0 } else { 2 }, format!("{r}'s maximum operating temperature"), json!("125 °C (default)"), "default", Some(r), "operating temperature range, including the part's own rise (inductors often 125 or 150 °C, some 105 °C)", vec!["hot".into(), r.into(), "tjMaxC".into()], format!("{r}'s temperature margin ({} °C now)", j["marginC"]), Some(format!("{} datasheet: maximum operating temperature, °C", mpn(r)))); } continue; }
        if !has(raw, &["hot", r, "tjMaxC"]) { item(if j["marginC"].as_f64().unwrap_or(99.0) < 25.0 { 0 } else { 2 }, format!("{r}'s maximum junction temperature"), json!("125 °C (default)"), "default", Some(r), "absolute maximum ratings: operating junction temperature", vec!["hot".into(), r.into(), "tjMaxC".into()], format!("{r}'s junction margin ({} °C now): many power parts allow 150 °C, many passives 105 °C", j["marginC"]), Some(format!("{} datasheet: TJ max, °C", mpn(r)))); }
        if j["thetaJcIncluded"] == false {
            let w = j["watts"].as_f64().unwrap_or(0.0);
            item(if w * 30.0 > j["marginC"].as_f64().unwrap_or(99.0) { 0 } else { 1 }, format!("{r}'s junction-to-case resistance"), json!("not included: the junction is taken at the tab's solved temperature"), "missing", Some(r), "thermal information: RθJC (bottom, or top for a part cooled from above)", vec!["hot".into(), r.into(), "thetaJcKPerW".into()], format!("{r}'s junction temperature is low by {w} W × θJC: +{:.0} to +{:.0} °C for 2 to 30 K/W", w * 2.0, w * 30.0), Some(format!("{} datasheet: RθJC, K/W", mpn(r))));
        }
    }
    // the dissipating parts with no watts in the spec: power devices by reference letter, inductors and regulators
    let hot_given: Vec<&String> = spec.hot.keys().collect();
    for (r, v) in &b.values { if (r.starts_with('Q') || (r.starts_with('U') && b.pads.iter().filter(|p| p.reference == *r).count() <= 8) || r.starts_with('L')) && !hot_given.contains(&r) && chips.as_array().map_or(true, |a| !a.iter().any(|c| c["reference"] == r.as_str())) {
        if spec.hot.is_empty() && r.starts_with('U') { continue; }
        item(2, format!("{r}'s dissipation"), json!("0 W: not a heat source in this solve"), "missing", Some(r), "power dissipation at the operating point (efficiency curve, RDS(on) × I², DCR × I², or quiescent current × V)", vec!["hot".into(), r.into(), "watts".into()], format!("the board's temperatures, if {r} ({v}) dissipates more than a few tens of mW"), None);
    } }
    // capacitors whose temperature rating was assumed
    let assumed: Vec<String> = rev["checks"]["capacitors"].as_array().into_iter().flatten().filter(|c| c["ratingAssumed"] == true).filter_map(|c| c["ref"].as_str().map(String::from)).collect();
    if !assumed.is_empty() { item(2, format!("the temperature rating of {} capacitor(s): {}", assumed.len(), assumed.iter().take(8).cloned().collect::<Vec<_>>().join(", ")), json!("85 °C for ceramics and others, 105 °C for electrolytics (assumed from the value text)"), "estimate", None, "operating temperature range (X5R 85 °C, X7R 125 °C; electrolytic life at its rated temperature)", vec!["values: put the dielectric or rating in the part's value or MPN".into()], "the capacitor-temperature findings".into(), None); }
    // ---- the board and its world
    let sf = rev["stackupFrom"].as_str().unwrap_or("");
    if sf == "default" { item(0, "the dielectric thicknesses".into(), json!(spec.real_gaps.clone()), "default", None, "the fab's stackup drawing (prepreg and core thicknesses)", vec!["stackupMm".into()], "plane capacitance, impedances, hot-loop and plane-to-plane coupling, heat through the board".into(), Some("fab stackup: dielectric mm per gap".into())); }
    if !has(raw, &["epsilonR"]) && b.stack_er.is_none() { item(1, "the laminate's dielectric constant".into(), json!(4.3), "default", None, "the laminate datasheet's Dk at 1 GHz (FR-4 runs 3.8 to 4.6)", vec!["epsilonR".into()], "every impedance: Z0 moves about ±3 % across FR-4's range, and the plane resonances with it".into(), Some("laminate datasheet: Dk at 1 GHz".into())); }
    if !has(raw, &["copperUm"]) && spec.copperUmByLayer.is_empty() { item(0, "the copper thickness".into(), json!(format!("{} µm", spec.copperUm)), "default", None, "the fab order: outer and inner copper weight (1 oz is 35 µm before plating)", vec!["copperUm".into()], "current density, IR drop, copper loss and the heat spreading: twice the copper halves the density".into(), Some("fab: finished copper, µm".into())); }
    if !has(raw, &["ambientC"]) { item(2, "the ambient temperature".into(), json!("25 °C"), "default", None, "the product's environment (an enclosure is often 40 to 50 °C inside)", vec!["ambientC".into()], "every absolute temperature and junction margin moves one for one".into(), Some("product: worst-case ambient, °C".into())); }
    for row in rev["cableEmissions"]["connectors"].as_array().into_iter().flatten() { let cj = row["connector"].as_str().unwrap_or(""); if !has(raw, &["cables", cj]) {
        let f = row["atMHz"].as_f64().unwrap_or(100.0) * 1e6; let q = 3e8 / f / 4.0; let d2 = r1(20.0 * ((2f64).min(q) / (1f64).min(q)).log10());
        let m = row["worstMarginDb"].as_f64().unwrap_or(99.0);
        item(if m < 6.0 && d2 >= 3.0 { 0 } else if m < 6.0 { 1 } else { 2 }, format!("the cable length on {cj}"), json!("1 m (default)"), "default", Some(cj), "the product: the cable that plugs into it in use and in the lab", vec!["cables".into(), cj.into()], format!("{cj}'s common-mode emission: a 2 m cable reads {d2} dB higher at {:.0} MHz (no gain past a quarter wave)", f / 1e6), Some("product: cable length, m".into()));
    } }
    for rail in rev["pdn"]["rails"].as_array().into_iter().flatten().filter(|r| r["skipped"].is_null()) {
        let n = rail["net"].as_str().unwrap_or("");
        if !has(raw, &["pdn", n, "stepA"]) && !has(raw, &["pdn", n, "targetOhm"]) { item(1, format!("{n}'s load step and tolerance"), json!(format!("{} A step, {} % (defaults)", rail["stepA"].as_f64().map(r1).map_or("?".into(), |v| v.to_string()), rail["tolPct"].as_f64().unwrap_or(5.0))), "default", rail["load"].as_str().map(|l| l.split('.').next().unwrap_or(l)).filter(|r| !(r.starts_with('J') || r.starts_with('P') || r.starts_with("MP"))), "the load's datasheet: supply current transient (or its max current) and its supply tolerance", vec!["pdn".into(), n.into(), "stepA".into()], format!("{n}'s target impedance: the whole PDN pass or fail scales with it"), Some("load datasheet: worst current step, A".into())); }
    }
    if rev["rf"]["loLeak"].as_array().map_or(false, |a| !a.is_empty()) {
        if !has(raw, &["rf", "cbcPf"]) { item(0, "the transistors' collector-base capacitance".into(), json!("0.7 pF (default)"), "default", None, "Cob or Ccb (output capacitance at the bias, from the small-signal table)", vec!["rf".into(), "cbcPf".into()], "the LO leak out of the antenna: leak current through Ccb scales with it, about 20·log(Cnew/0.7) dB".into(), Some("transistor datasheet: Cob, pF".into())); }
        if !has(raw, &["rf", "cbePf"]) { item(1, "the transistors' base-emitter capacitance".into(), json!("2 pF (default)"), "default", None, "Cib or Cbe (input capacitance)", vec!["rf".into(), "cbePf".into()], "the LO leak's divider into the antenna".into(), Some("transistor datasheet: Cib, pF".into())); }
    }
    for n in rev["reflections"]["nets"].as_array().into_iter().flatten().filter(|n| n["ran"] == true && n["electricallyShort"] == false) {
        let net = n["net"].as_str().unwrap_or(""); let drv = n["driver"].as_str().unwrap_or("").split('.').next().unwrap_or("").to_string();
        let over = n["inUse"]["overshootPct"].as_f64().unwrap_or(0.0);
        if !has(raw, &["tdr", net, "driverOhm"]) || !has(raw, &["tdr", net, "riseNs"]) { item(if over > 15.0 { 0 } else { 1 }, format!("{net}'s driver: output resistance and edge"), json!(format!("{} Ω, {} ns", n["driverOhm"], n["edgeNs"])), "default", Some(drv.as_str()), "the pin's output impedance and rise time at its drive-strength setting (the IBIS model, or VOH/IOH and the slew setting)", vec!["tdr".into(), net.into(), "driverOhm".into()], format!("{net}'s overshoot ({over:.0} % now) and the series resistor it needs"), Some(format!("{} datasheet or IBIS: output impedance, Ω", mpn(&drv)))); }
    }
    items.sort_by_key(|x| x.0);
    let rank = |r: u8| match r { 0 => "high", 1 => "medium", _ => "low" };
    let list: Vec<Value> = items.iter().map(|(r, v)| { let mut v = v.clone(); v["matters"] = json!(rank(*r)); v }).collect();
    json!({"items": list, "counts": {"high": items.iter().filter(|x| x.0 == 0).count(), "medium": items.iter().filter(|x| x.0 == 1).count(), "low": items.iter().filter(|x| x.0 == 2).count()}, "specPatch": patch,
        "howTo": "Read each part's datasheet for its datasheetField (the MPN is in part.mpn), put the number at specKey (specPatch has the shape, each value a placeholder naming its source), and re-run; the item leaves this list once the spec gives it."})
}