app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Drive your own KiCad from an AI, in the background: open every editor, read the whole board as data (the EDA Bridge Standard board model), stack-up, part heights, netlists, place, route, pour, DRC and exports.
← Commit history
Merge PR #337: kicad-lane: 1.0.79, simulate in the standard's shape (#23)
8 files changed
+208−29
rust/Cargo.lock+3−3rust/Cargo.toml+1−1rust/crates/kicad-bridge/src/kicad_only.txt−2rust/crates/kicad-bridge/src/standard_names.txt+3−1rust/crates/kicad-bridge/src/verbs_sim.rs+21−10rust/crates/kicad-core/src/spice.rs+164skills/kicad-bridge-test/run_verb_tests.py+6−5skills/kicad-schematic-and-board-edits/SKILL.md+10−7rust/Cargo.lock+3−3@@ -319,7 +319,7 @@ dependencies = [ [[package]] name = "kicad-bridge"-version = "1.0.78"+version = "1.0.79" dependencies = [ "adom-eda-checks", "kicad-core",@@ -331,7 +331,7 @@ dependencies = [ [[package]] name = "kicad-core"-version = "1.0.78"+version = "1.0.79" dependencies = [ "kicad-ipc-rs", "kicad-platform",@@ -358,7 +358,7 @@ dependencies = [ [[package]] name = "kicad-platform"-version = "1.0.78"+version = "1.0.79" dependencies = [ "serde", "serde_json",
rust/Cargo.toml+1−1@@ -3,7 +3,7 @@ resolver = "2" members = ["crates/*"] [workspace.package]-version = "1.0.78"+version = "1.0.79" edition = "2021" license = "MIT" publish = false
rust/crates/kicad-bridge/src/kicad_only.txt−2@@ -108,5 +108,3 @@ kicad_upgrade kicad_validate_for_fab kicad_verb_times kicad_zone_state-kicad_export_spice-kicad_simulate
rust/crates/kicad-bridge/src/standard_names.txt+3−1@@ -1,4 +1,4 @@-# The EDA Bridge Standard's verb names (adom/eda-bridge-standard verbs/standard.json, reference altium 0.2.76). Generated by tools/sync_standard_names.py; do not edit.+# The EDA Bridge Standard's verb names (adom/eda-bridge-standard verbs/standard.json, reference altium 0.2.79). Generated by tools/sync_standard_names.py; do not edit. a365_configure a365_files_upload a365_library_overview@@ -85,6 +85,7 @@ export_library_part export_manufacturing export_odb export_pdf+export_spice export_step extension_ping extract_intlib@@ -236,6 +237,7 @@ signin_status silk_designators silk_state silkscreen_prep+simulate state status swap_footprint
rust/crates/kicad-bridge/src/verbs_sim.rs+21−10@@ -29,9 +29,9 @@ pub static VERBS: &[Verb] = &[ name: "kicad_simulate", summary: "Simulate a schematic with SPICE, headless: KiCad's netlist of the sheet run by the ngspice library KiCad installs, with the analysis you give (transient, AC, DC sweep, operating point) or the one the schematic carries; each probe comes back downsampled with its first, last, min and max values (AC as dB and phase), and as a CSV file.", mechanism: Mechanism::Cli, risk: "write", timeout_sec: 300,- input: "{\"schPath\": \"C:/.../x.kicad_sch\", \"analysis\"?: \".tran 10u 5m\" | [\".op\", ...], \"probes\"?: [\"V(/OUT)\", \"I(V1)\"], \"points\"?: 200, \"outputDir\"?: \"C:/...\", \"timeoutSec\"?: 120}",- example: "kicad_simulate {\"schPath\":\"C:/Users/john/proj/filter.kicad_sch\",\"analysis\":\".ac dec 20 10 1meg\",\"probes\":[\"V(/OUT)\"]}",- hint: "probes are node voltages V(/NET) (or the vector names in available) and source currents I(V1); without probes every node and branch comes back (at most 24). sweep is time, frequency or the DC sweep. An AC probe has db and phaseDeg. csvPath holds the same points. Read log for ngspice's own messages when a run fails.",+ input: "{\"schPath\" | \"schematic\": \"C:/.../x.kicad_sch\", \"analysis\"?: {\"type\": \"op\"} | {\"type\": \"dc\", \"source\", \"start\", \"stop\", \"step\"} | {\"type\": \"ac\", \"sweep\": \"dec|oct|lin\", \"points\", \"start\", \"stop\"} | {\"type\": \"tran\", \"step\", \"stop\", \"start\"?} | {\"type\": \"raw\", \"text\": \".noise ...\"} | \".tran 10u 5m\" (or a list), \"probes\"?: [{\"net\": \"OUT\"}, {\"current\": \"V1\"}] | [\"V(/OUT)\"], \"points\"?: 200, \"outputDir\"?: \"C:/...\", \"timeoutSec\"?: 120}",+ example: "kicad_simulate {\"schPath\":\"C:/Users/john/proj/filter.kicad_sch\",\"analysis\":{\"type\":\"ac\",\"sweep\":\"dec\",\"points\":20,\"start\":10,\"stop\":1000000},\"probes\":[{\"net\":\"OUT\"}]}",+ hint: "signals[{signal, x, y, xUnit, yUnit, phase?}] is the standard's answer (#23): each probe's own points, at most points of them with the first, last and extreme points kept; AC as dB with unwrapped phase in degrees. engine names the ngspice and KiCad versions. probes are nets ({net: \"OUT\"}, or V(/OUT)) and source currents ({current: \"V1\"}); without probes every node and branch comes back (at most 24). sweep is time, frequency or the DC sweep. An AC probe has db and phaseDeg. csvPath holds the same points. Read log for ngspice's own messages when a run fails.", related: &["kicad_export_spice", "kicad_build_schematic", "kicad_sch_set_property"], pitfalls: &["Every part needs a SPICE model: R, C and L work from their values; sources come from the Simulation_SPICE library; ICs need a model file (Sim.Library).", "Runs on Windows through KiCad's ngspice.dll.", "A long transient with a tiny step returns many points: points downsamples the reply, not the run."], },@@ -64,7 +64,7 @@ fn export(state: &mut State, sch: &Path, out: &Path, format: &str) -> Result<(St } fn sch_arg(args: &Value) -> Result<PathBuf, Value> {- let Some(p) = arg_any(args, &["schPath", "filePath"]).map(PathBuf::from) else {+ let Some(p) = arg_any(args, &["schPath", "schematic", "filePath"]).map(PathBuf::from) else { return Err(fail("missing_arg", "schPath is required", "The .kicad_sch to simulate (the root sheet of a project).")); }; if !p.is_file() {@@ -85,6 +85,7 @@ fn export_spice(state: &mut State, args: &Value) -> Result<Value, Value> { v["success"] = json!(true); v["path"] = json!(norm(&out)); v["format"] = json!(format);+ v["dialect"] = json!("ngspice"); // KiCad writes ngspice's SPICE (#23) v["output"] = json!(format!("SPICE netlist of {} written to {}", norm(&sch), norm(&out))); v["_hint"] = json!("kicad_simulate {schPath, analysis, probes} runs it through KiCad's ngspice."); Ok(v)@@ -92,12 +93,15 @@ fn export_spice(state: &mut State, args: &Value) -> Result<Value, Value> { fn simulate(state: &mut State, args: &Value) -> Result<Value, Value> { let sch = sch_arg(args)?;- let analyses: Vec<String> = match args.get("analysis") {- Some(Value::String(s)) => vec![s.clone()],- Some(Value::Array(a)) => a.iter().filter_map(Value::as_str).map(String::from).collect(),+ // the standard's analysis ({type: op|dc|ac|tran|raw, ...}, #23) or SPICE text, one or a list+ let given: Vec<Value> = match args.get("analysis") {+ Some(Value::Array(a)) => a.clone(),+ Some(v) if !v.is_null() => vec![v.clone()], _ => Vec::new(), };- let probes: Vec<String> = args.get("probes").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_str).map(String::from).collect()).unwrap_or_default();+ let analyses: Vec<String> = given.iter().map(spice::analysis_text).collect::<Result<_, _>>().map_err(|e| fail("bad_args", e, "analysis: {type: \"ac\", sweep: \"dec\", points: 20, start: 10, stop: 1e6}, {type: \"tran\", step: 1e-5, stop: 5e-3}, {type: \"dc\", source: \"V1\", start, stop, step}, {type: \"op\"} or {type: \"raw\", text: \".noise ...\"}."))?;+ // probes: {net: "OUT"} | {current: "V1"} | "V(/OUT)", as ngspice's vector names+ let probes: Vec<String> = args.get("probes").and_then(Value::as_array).map(|a| a.iter().filter_map(spice::probe_vector).collect()).unwrap_or_default(); let points = args.get("points").and_then(Value::as_u64).unwrap_or(200) as usize; let tmp = std::env::temp_dir().join(format!("adom-kicad-sim-{}-{}", std::process::id(), std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis()).unwrap_or(0))); let _ = std::fs::create_dir_all(&tmp);@@ -123,7 +127,11 @@ fn simulate(state: &mut State, args: &Value) -> Result<Value, Value> { v["netlist"] = json!(lines); return Err(v); }- let mut v = spice::summarize(&run, &probes, points).map_err(|e| fail("no_such_probe", e, "Probe node voltages as V(/NET) and source currents as I(V1); available lists this run's vectors."))?;+ let mut v = spice::summarize(&run, &probes, points).map_err(|e| fail("no_such_probe", e, "Probe nets as {net: \"OUT\"} (or V(/OUT)) and source currents as {current: \"V1\"}; available lists this run's vectors."))?;+ // the standard's signals (#23): each probe's own x and y, the first, last and extreme points kept+ let (signals, missing) = spice::signals(&run, &probes, points);+ v["signals"] = json!(signals);+ v["missing"] = json!(missing); let out_dir = arg_str(args, "outputDir").map(PathBuf::from).unwrap_or_else(|| sch.parent().unwrap_or(Path::new(".")).join("sim_output")); let _ = std::fs::create_dir_all(&out_dir); let stem = sch.file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_else(|| "sim".into());@@ -134,7 +142,10 @@ fn simulate(state: &mut State, args: &Value) -> Result<Value, Value> { v["analysis"] = json!(lines.iter().filter(|l| l.starts_with('.') && !l.eq_ignore_ascii_case(".end") && !l.to_lowercase().starts_with(".title") && !l.to_lowercase().starts_with(".include")).collect::<Vec<_>>()); v["log"] = json!(run["log"].as_array().map(|l| l.iter().rev().take(12).rev().cloned().collect::<Vec<_>>()).unwrap_or_default()); v["seconds"] = json!((started.elapsed().as_secs_f64() * 100.0).round() / 100.0);- v["engine"] = json!("ngspice (KiCad's ngspice.dll)");+ // the simulator and its version, so runs from two tools can be compared (#23)+ let ng = run["log"].as_array().into_iter().flatten().filter_map(Value::as_str).find_map(|l| l.split("ngspice-").nth(1).and_then(|t| t.split_whitespace().next()).map(String::from));+ let kicad = state.kicad_info().primary().map(|p| p.cli_version.clone()).unwrap_or_default();+ v["engine"] = json!(format!("ngspice {} (KiCad {})", ng.unwrap_or_else(|| "?".into()), if kicad.is_empty() { "?" } else { kicad.as_str() })); v["output"] = json!(format!("{} vector(s) of {} over {} point(s)", v["probes"].as_array().map_or(0, |a| a.len()), v["plot"].as_str().unwrap_or(""), v["of"])); v["_hint"] = json!("probes[].values follow sweep.values (AC: db and phaseDeg). csvPath holds the same points; pull it with ab pull_file. missing lists probes this run did not have."); Ok(v)
rust/crates/kicad-core/src/spice.rs+164@@ -135,6 +135,143 @@ pub fn summarize(run: &Value, probes: &[String], points: usize) -> Result<Value, "available": vecs.iter().map(name).take(60).collect::<Vec<_>>()})) } +/// One analysis in the standard's form (#23) as SPICE text: `{type: op}`, `{type: dc, source, start, stop, step}`,+/// `{type: ac, sweep: dec|oct|lin, points, start, stop}`, `{type: tran, step, stop, start?}` or `{type: raw, text}`;+/// a string is taken as SPICE text.+pub fn analysis_text(a: &Value) -> Result<String, String> {+ if let Some(t) = a.as_str() {+ return Ok(t.trim().to_string());+ }+ let num = |k: &str| a.get(k).and_then(Value::as_f64).ok_or(format!("analysis {} needs {k}", a["type"].as_str().unwrap_or("?")));+ let g = |v: f64| format!("{v}");+ match a["type"].as_str().unwrap_or("") {+ "op" => Ok(".op".into()),+ "raw" => a["text"].as_str().map(|t| t.trim().to_string()).ok_or("a raw analysis carries its SPICE text in text".into()),+ "dc" => Ok(format!(".dc {} {} {} {}", a["source"].as_str().ok_or("a dc analysis names its source")?, g(num("start")?), g(num("stop")?), g(num("step")?))),+ "ac" => {+ let sweep = a["sweep"].as_str().unwrap_or("dec");+ if !["dec", "oct", "lin"].contains(&sweep) {+ return Err("an ac sweep is dec, oct or lin".into());+ }+ Ok(format!(".ac {sweep} {} {} {}", num("points")? as u64, g(num("start")?), g(num("stop")?)))+ }+ "tran" => Ok(match a.get("start").and_then(Value::as_f64) {+ Some(st) => format!(".tran {} {} {}", g(num("step")?), g(num("stop")?), g(st)),+ None => format!(".tran {} {}", g(num("step")?), g(num("stop")?)),+ }),+ t => Err(format!("analysis type {t:?} is not op, dc, ac, tran or raw")),+ }+}++/// A probe in the standard's form, `{net: "OUT"}` or `{current: "V1"}`, or a string (`V(/OUT)`, `I(V1)`), as+/// the vector name ngspice gives it.+pub fn probe_vector(p: &Value) -> Option<String> {+ if let Some(s) = p.as_str() {+ return Some(vector_name(s));+ }+ if let Some(n) = p["net"].as_str() {+ return Some(n.trim().to_lowercase());+ }+ p["current"].as_str().map(|c| format!("{}#branch", c.trim().to_lowercase()))+}++/// Indices to keep from `y` within `points` (a ceiling): the first and last points and, per bucket, the lowest+/// and highest, so no peak is decimated away (#23).+pub fn keep_extrema(y: &[f64], points: usize) -> Vec<usize> {+ let n = y.len();+ if n <= points.max(2) {+ return (0..n).collect();+ }+ let buckets = ((points.max(4) - 2) / 2).max(1);+ let mut idx = vec![0, n - 1];+ let inner = n - 2;+ for b in 0..buckets {+ let (lo, hi) = (1 + b * inner / buckets, 1 + (b + 1) * inner / buckets);+ if lo >= hi {+ continue;+ }+ let (mut imin, mut imax) = (lo, lo);+ for i in lo..hi {+ if y[i] < y[imin] {+ imin = i;+ }+ if y[i] > y[imax] {+ imax = i;+ }+ }+ idx.push(imin);+ idx.push(imax);+ }+ idx.sort_unstable();+ idx.dedup();+ idx+}++/// Phase in degrees with its 360-degree jumps removed.+pub fn unwrap_deg(p: &[f64]) -> Vec<f64> {+ let mut out: Vec<f64> = Vec::with_capacity(p.len());+ let mut offset = 0.0f64;+ for (i, v) in p.iter().enumerate() {+ if i > 0 {+ let d = v + offset - out[i - 1];+ if d > 180.0 {+ offset -= 360.0 * ((d + 180.0) / 360.0).floor();+ } else if d < -180.0 {+ offset += 360.0 * ((-d + 180.0) / 360.0).floor();+ }+ }+ out.push(v + offset);+ }+ out+}++/// The run's signals in the standard's shape (#23): `[{signal, x, y, xUnit, yUnit, phase?}]`, each decimated on its+/// own within `points`, keeping its first, last and extreme points; AC as dB with unwrapped phase in degrees.+/// `wanted` are vector names (probe_vector); empty means every node and branch (24 at most).+pub fn signals(run: &Value, wanted: &[String], points: usize) -> (Vec<Value>, Vec<String>) {+ let vecs = run["vectors"].as_array().cloned().unwrap_or_default();+ let name = |v: &Value| v["name"].as_str().unwrap_or("").to_lowercase();+ let is_sweep = |n: &str| ["time", "frequency"].contains(&n) || n.ends_with("-sweep");+ let sweep = vecs.iter().find(|v| is_sweep(&name(v)));+ let sn = sweep.map(name).unwrap_or_default();+ let x_unit = match sn.as_str() { "time" => "s", "frequency" => "Hz", "i-sweep" => "A", "temp-sweep" => "C", "" => "", _ => "V" };+ let nums = |v: &Value, k: &str| -> Vec<f64> { v[k].as_array().map(|a| a.iter().map(|x| x.as_f64().unwrap_or(f64::NAN)).collect()).unwrap_or_default() };+ let x_all: Vec<f64> = sweep.map(|s| if s["real"].is_array() { nums(s, "real") } else { nums(s, "re") }).unwrap_or_default();+ let mut missing = Vec::new();+ let chosen: Vec<&Value> = if wanted.is_empty() {+ vecs.iter().filter(|v| !is_sweep(&name(v))).take(24).collect()+ } else {+ let mut c = Vec::new();+ for w in wanted {+ match vecs.iter().find(|v| name(v) == *w || name(v).trim_start_matches('/') == w.trim_start_matches('/')) {+ Some(v) => c.push(v),+ None => missing.push(w.clone()),+ }+ }+ c+ };+ let label = |n: &str| if n.ends_with("#branch") { format!("I({})", n.trim_end_matches("#branch").to_uppercase()) } else { format!("V({})", n.trim_start_matches('/').to_uppercase()) };+ let mut out = Vec::new();+ for v in chosen {+ let n = name(v);+ let current = n.ends_with("#branch");+ if v["real"].is_array() {+ let y = nums(v, "real");+ let idx = keep_extrema(&y, points);+ let x: Vec<f64> = if x_all.len() == y.len() { idx.iter().map(|&i| r(x_all[i])).collect() } else { idx.iter().map(|&i| i as f64).collect() };+ out.push(json!({"signal": label(&n), "x": x, "y": idx.iter().map(|&i| r(y[i])).collect::<Vec<_>>(), "xUnit": x_unit, "yUnit": if current { "A" } else { "V" }}));+ } else {+ let (re, im) = (nums(v, "re"), nums(v, "im"));+ let db: Vec<f64> = re.iter().zip(&im).map(|(a, b)| 20.0 * a.hypot(*b).log10()).collect();+ let ph = unwrap_deg(&re.iter().zip(&im).map(|(a, b)| b.atan2(*a).to_degrees()).collect::<Vec<_>>());+ let idx = keep_extrema(&db, points);+ let x: Vec<f64> = if x_all.len() == db.len() { idx.iter().map(|&i| r(x_all[i])).collect() } else { idx.iter().map(|&i| i as f64).collect() };+ out.push(json!({"signal": label(&n), "x": x, "y": idx.iter().map(|&i| r(db[i])).collect::<Vec<_>>(), "phase": idx.iter().map(|&i| r(ph[i])).collect::<Vec<_>>(), "xUnit": x_unit, "yUnit": "dB"}));+ }+ }+ (out, missing)+}+ /// The summary as CSV: the sweep, then a column per probe (dB and phase for a complex one). pub fn csv(summary: &Value) -> String { let sweep: Vec<f64> = summary["sweep"]["values"].as_array().map(|a| a.iter().filter_map(Value::as_f64).collect()).unwrap_or_default();@@ -169,6 +306,33 @@ pub fn csv(summary: &Value) -> String { mod tests { use super::*; + #[test]+ fn the_standard_shape_of_analyses_probes_and_signals() {+ assert_eq!(analysis_text(&json!({"type": "ac", "sweep": "dec", "points": 20, "start": 10, "stop": 1000000})).unwrap(), ".ac dec 20 10 1000000");+ assert_eq!(analysis_text(&json!({"type": "tran", "step": 1e-5, "stop": 0.005})).unwrap(), ".tran 0.00001 0.005");+ assert_eq!(analysis_text(&json!({"type": "dc", "source": "V1", "start": 0, "stop": 5, "step": 0.1})).unwrap(), ".dc V1 0 5 0.1");+ assert_eq!(analysis_text(&json!({"type": "op"})).unwrap(), ".op");+ assert_eq!(analysis_text(&json!({"type": "raw", "text": ".noise v(out) V1 dec 10 1 1meg"})).unwrap(), ".noise v(out) V1 dec 10 1 1meg");+ assert!(analysis_text(&json!({"type": "ac", "points": 20})).is_err());+ assert_eq!(probe_vector(&json!({"net": "OUT"})).unwrap(), "out");+ assert_eq!(probe_vector(&json!({"current": "V1"})).unwrap(), "v1#branch");+ // a spike survives decimation+ let mut y = vec![0.0; 1000];+ y[517] = 5.0;+ let k = keep_extrema(&y, 20);+ assert!(k.len() <= 20 && k.contains(&517) && k.contains(&0) && k.contains(&999), "{k:?}");+ assert_eq!(unwrap_deg(&[170.0, -170.0, -150.0]), vec![170.0, 190.0, 210.0]);+ let run = json!({"vectors": [{"name": "frequency", "re": [10.0, 100.0], "im": [0.0, 0.0]}, {"name": "/out", "re": [1.0, 0.0], "im": [0.0, -0.1]}, {"name": "v1#branch", "re": [0.001, 0.001], "im": [0.0, 0.0]}]});+ let (sig, missing) = signals(&run, &["out".into(), "nope".into()], 200);+ assert_eq!(sig[0]["signal"], "V(OUT)");+ assert_eq!((sig[0]["xUnit"].as_str(), sig[0]["yUnit"].as_str()), (Some("Hz"), Some("dB")));+ assert_eq!(sig[0]["y"], json!([0.0, -20.0]));+ assert_eq!(sig[0]["phase"], json!([0.0, -90.0]));+ assert_eq!(missing, vec!["nope".to_string()]);+ let (all, _) = signals(&run, &[], 200);+ assert!(all.iter().any(|s| s["signal"] == "I(V1)" && s["yUnit"] == "dB"));+ }+ const NET: &str = ".title KiCad schematic\nV1 /IN 0 DC 0 SIN( 0 1 1k ) AC 1\nR1 /IN /OUT 1k\nC1 /OUT 0 100n\n.tran 1u 2m\n.end\n"; #[test]
skills/kicad-bridge-test/run_verb_tests.py+6−5@@ -1593,10 +1593,11 @@ def main(): {"ref": "R1", "symbol": "Device:R", "comment": "1k"}, {"ref": "C1", "symbol": "Device:C", "comment": "100n"}], "nets": {"IN": ["V1.1", "R1.1"], "OUT": ["R1.2", "C1.1"], "GND": ["V1.2", "C1.2"]}}, 400) ex = C("export_spice", {"schPath": rc}, 200)- own("export_spice", ex, ok(ex) and ex.get("elements") == 3 and (ex.get("path") or "").endswith(".cir"), "no 3-element SPICE netlist of the RC")- ac = C("simulate", {"schPath": rc, "analysis": ".ac dec 20 10 100k", "probes": ["V(/OUT)"], "points": 500}, 300)- f = ((ac.get("sweep") or {}).get("values") or [])- db = (((ac.get("probes") or [{}])[0]).get("db") or [])+ own("export_spice", ex, ok(ex) and ex.get("elements") == 3 and (ex.get("path") or "").endswith(".cir") and ex.get("dialect") == "ngspice", "no 3-element ngspice netlist of the RC")+ # the standard's shape (#23, 1.0.79): structured analysis, probes by net, signals[{signal, x, y}]+ ac = C("simulate", {"schematic": rc, "analysis": {"type": "ac", "sweep": "dec", "points": 20, "start": 10, "stop": 100000}, "probes": [{"net": "OUT"}], "points": 500}, 300)+ sig = ((ac.get("signals") or [{}])[0])+ f, db = sig.get("x") or [], sig.get("y") or [] # the crossing interpolated in log frequency: at 20 points a decade the grid steps past it (1584.9 Hz is -2.99 dB) f3 = None for i in range(len(min(f, db, key=len)) - 1):@@ -1605,7 +1606,7 @@ def main(): f3 = 10 ** (math.log10(f[i]) + t * (math.log10(f[i + 1]) - math.log10(f[i]))) break bad = C("simulate", {"schPath": rc, "analysis": ".dc V9 0 1 0.1"}, 200)- own("simulate", {"ac": ac, "bad": bad}, ok(ac) and f3 is not None and abs(f3 - 1591.5) / 1591.5 < 0.02 and ac.get("csvPath") and bad.get("errorCode") == "simulation_failed",+ own("simulate", {"ac": ac, "bad": bad}, ok(ac) and sig.get("signal") == "V(OUT)" and sig.get("yUnit") == "dB" and str(ac.get("engine") or "").startswith("ngspice ") and f3 is not None and abs(f3 - 1591.5) / 1591.5 < 0.02 and ac.get("csvPath") and bad.get("errorCode") == "simulation_failed", f"the RC's -3 dB point was {f3} Hz, not about 1592, or a failing analysis did not fail") pr = C("plan_route", {"filePath": CK, "source": "file", "net": "/DRV_GHC", "from": "R12.1", "to": "U4.13"}, 200) rn = pr.get("routeNet") or {}
skills/kicad-schematic-and-board-edits/SKILL.md+10−7@@ -29,16 +29,19 @@ Every verb here edits files or the live board the way KiCad would, answers what `kicad_simulate {schPath, analysis, probes}` runs the sheet through ngspice, the simulator KiCad itself uses (KiCad's own ngspice.dll on Windows), in a child process so a bad circuit cannot take the bridge down:-- `analysis`: `".tran 10u 5m"`, `".ac dec 20 10 1meg"`, `".dc V1 0 5 0.1"` or `".op"` (or a list); without- it, the analysis the schematic carries runs.-- `probes`: node voltages `V(/OUT)` and source currents `I(V1)`; without them every node and branch comes- back (24 at most). `available` lists this run's vectors.-- Each probe comes back downsampled to `points` (200) with its first, last, min and max; an AC probe as- `db` and `phaseDeg`. `csvPath` holds the same points; `sweep` is time, frequency or the DC sweep.+- `analysis` in the EDA Bridge Standard's form: `{type: "ac", sweep: "dec", points: 20, start: 10, stop: 1e6}`,+ `{type: "tran", step: 1e-5, stop: 5e-3}`, `{type: "dc", source: "V1", start, stop, step}`, `{type: "op"}`,+ or `{type: "raw", text: ".noise ..."}`; SPICE text (`".tran 10u 5m"`) works too. Without it, the analysis+ the schematic carries runs.+- `probes`: `[{net: "OUT"}, {current: "V1"}]` (or `"V(/OUT)"`); without them every node and branch comes back+ (24 at most). `available` lists this run's vectors.+- `signals[{signal, x, y, xUnit, yUnit, phase?}]` is the answer: each probe's own points, at most `points`+ (200) of them with the first, last and extreme points always kept; AC as dB with unwrapped phase in degrees.+ `engine` names the ngspice and KiCad versions; `csvPath` holds the points. Every part needs a SPICE model: R, C and L work from their values, sources come from the `Simulation_SPICE` library (VSIN, VDC, VPULSE...), ICs need a model file (their Sim.Library field). `kicad_export_spice` writes-KiCad's netlist (`.cir`) and says what it holds. Checked: an RC of 1k and 100n is -3 dB at 1.58 kHz, -45 deg.+KiCad's netlist (`.cir`, `dialect: ngspice`) and says what it holds. Checked: an RC of 1k and 100n is -3 dB at 1.58 kHz, -45 deg. ## Board (`kicad_*` on a .kicad_pcb)