diff --git a/src/core/app/load.cpp b/src/core/app/load.cpp index 3a4f94e..2830144 100644 --- a/src/core/app/load.cpp +++ b/src/core/app/load.cpp @@ -36,6 +36,7 @@ LoadResult load_module(System *sys, const std::string &module_name, r.power = inf.power; r.gnd = inf.gnd; r.kept_other = inf.kept_other; + r.adjacent = inf.adjacent; r.ok = true; } catch (const std::exception &e) { r.error = e.what(); diff --git a/src/core/app/load.hpp b/src/core/app/load.hpp index 93308c3..26420a7 100644 --- a/src/core/app/load.hpp +++ b/src/core/app/load.hpp @@ -23,6 +23,7 @@ struct LoadResult { int power = 0; ///< signals inferred Power (name + structure) int gnd = 0; ///< signals inferred GndShield (name) int kept_other = 0; ///< name said Power but evidence too weak → kept Other + int adjacent = 0; ///< rail + control token (SENSE/EN/PG/…) → Other, not suspect }; // Import a module from a netlist/pinout file into `sys`, drop singleton signals, diff --git a/src/core/app/script.cpp b/src/core/app/script.cpp index 3e0eb65..27bde4b 100644 --- a/src/core/app/script.cpp +++ b/src/core/app/script.cpp @@ -189,7 +189,9 @@ private: + " singleton/NC signal(s))" : "")); emit(" types: " + std::to_string(r.power) + " power, " + std::to_string(r.gnd) + " gnd, " + std::to_string(r.kept_other) - + " suspect Power (name only — kept as Other)"); + + " suspect Power (name only — kept as Other), " + + std::to_string(r.adjacent) + + " power-adjacent (control — kept as Other)"); return true; } if (cmd == "connect" || cmd == "plug") { diff --git a/src/core/domain/analysis.cpp b/src/core/domain/analysis.cpp index 144069c..7d9550a 100644 --- a/src/core/domain/analysis.cpp +++ b/src/core/domain/analysis.cpp @@ -276,13 +276,21 @@ SignalTypeInferenceStats infer_signal_types(System *sys) { Module *mod = mkv.second; for (auto &skv : *mod->signals) { Signal *s = skv.second; - SignalType named = infer_signal_type(s->name); - if (named == SignalType::GndShield) { + NameClassification ncl = classify_signal_name(s->name); + if (ncl.verdict == NameVerdict::GndShield) { s->type = SignalType::GndShield; ++st.gnd; continue; } - if (named == SignalType::Power) { + if (ncl.verdict == NameVerdict::PowerAdjacent) { + // A rail token next to a control token (SENSE, EN, PG, …): + // a signal about a rail, confidently NOT the rail — never + // suspect, whatever the fan-out. + s->type = SignalType::Other; + ++st.adjacent; + continue; + } + if (ncl.verdict == NameVerdict::Rail) { int fanout = (int)s->size(); // Hard rule: a "power" net that touches fewer than three // pins cannot physically be a rail (a real rail goes to diff --git a/src/core/domain/analysis.hpp b/src/core/domain/analysis.hpp index 89d5b19..01ad450 100644 --- a/src/core/domain/analysis.hpp +++ b/src/core/domain/analysis.hpp @@ -62,6 +62,8 @@ struct SignalTypeInferenceStats { int power = 0; ///< Signals promoted to Power (name + structural). int gnd = 0; ///< Signals promoted to GndShield (name only). int kept_other = 0; ///< Name said Power but structural evidence too weak. + int adjacent = 0; ///< Rail token + control token (SENSE/EN/PG/…) → + ///< confidently Other, never suspect. }; // Thresholds used by `infer_signal_types` (re-exposed so the analyze screen diff --git a/src/core/domain/signal_type.hpp b/src/core/domain/signal_type.hpp index c95efdd..135575f 100644 --- a/src/core/domain/signal_type.hpp +++ b/src/core/domain/signal_type.hpp @@ -5,8 +5,24 @@ enum class SignalType { Power, GndShield, Other }; +// Name-level verdict, richer than SignalType. `PowerAdjacent` is the key +// addition: a name holding BOTH a rail token (VCC/VDD/PWR/…) and a control +// token (SENSE/EN/PG/FB/…) is a signal *about* a rail — measurement, enable, +// power-good — not the rail itself. Its non-Power classification is therefore +// confident, where a bare rail name without structural evidence stays suspect. +enum class NameVerdict { Rail, GndShield, PowerAdjacent, Other }; + +struct NameClassification { + NameVerdict verdict = NameVerdict::Other; + std::string token; ///< PowerAdjacent only: the control token that decided it. +}; + +NameClassification classify_signal_name(const std::string &signal_name); + const char *signal_type_name(SignalType t); bool signal_type_from_name(const std::string &s, SignalType &out); +// Thin wrapper over classify_signal_name: Rail → Power, GndShield → GndShield, +// PowerAdjacent/Other → Other. SignalType infer_signal_type(const std::string &signal_name); SignalType next_signal_type(SignalType t); // Power → GndShield → Other → Power diff --git a/src/core/domain/signals.cpp b/src/core/domain/signals.cpp index 0c77a03..3ebd27b 100644 --- a/src/core/domain/signals.cpp +++ b/src/core/domain/signals.cpp @@ -4,6 +4,7 @@ #include #include +#include #include const char *signal_type_name(SignalType t) { @@ -36,11 +37,68 @@ bool signal_type_from_name(const std::string &s, SignalType &out) { return false; } -// Heuristic. Names like GND, GNDA, SHIELD, CHASSIS → GndShield. -// Names containing PWR/POWER/VCC/VDD/VEE/VSS, or matching V±N or +N.NV -// patterns, or starting with VS_/VS3_ → Power. Else Other. -SignalType infer_signal_type(const std::string &name) { - if (name.empty()) return SignalType::Other; +namespace { + +// Control/monitoring vocabulary: a name holding both a rail token and one of +// these is a signal ABOUT a rail (feedback, enable, power-good, fault, …) — +// not the rail itself. Matched against whole separator-delimited tokens +// (uppercase, trailing digits stripped so EN1/PG0 still hit). Entries of +// length ≥ 4 also match as a token *suffix*, catching fused (VSENSE, PWRGOOD) +// and active-low (NFAULT) forms; short entries match exactly, so GREEN or +// SENSOR never trip on EN / SENSE. +const char *const kPowerControlTokens[] = { + "SENSE", "SNS", "KELVIN", // remote / Kelvin sense + "FB", "FDBK", "FEEDBACK", // regulator feedback + "EN", "ENA", "ENABLE", "INH", "INHIBIT", // enable / inhibit + "PG", "PGOOD", "PWRGD", "PWROK", // power-good + "GOOD", "OK", "FAIL", "FAULT", "FLT", // status / fault + "ALERT", "ALRT", "WARN", + "MON", "IMON", "VMON", "PMON", // monitoring + "DET", "DETECT", "PRSNT", "PRESENT", // presence detection + "OC", "OCP", "OV", "OVP", "UV", "UVP", // protection trips + "TRIP", "SHDN", "SHUTDOWN", + "SEQ", "CTRL", "CTL", // sequencing / control + "STAT", "STATUS", + "ON", "OFF", "BTN", // on/off request + "CS", "IRQ", +}; + +bool is_power_control_token(std::string tok) { + while (!tok.empty() && std::isdigit((unsigned char)tok.back())) + tok.pop_back(); // EN1, PG0, FB2 … + if (tok.empty()) return false; + for (const char *lex : kPowerControlTokens) { + size_t n = std::strlen(lex); + if (tok == lex) return true; + if (n >= 4 && tok.size() > n + && tok.compare(tok.size() - n, n, lex) == 0) + return true; // VSENSE, PWRGOOD, NFAULT … + } + return false; +} + +// Split on every non-alphanumeric character. `u` is already uppercase. +std::vector alnum_tokens(const std::string &u) { + std::vector out; + std::string cur; + for (char c : u) { + if (std::isalnum((unsigned char)c)) { cur += c; continue; } + if (!cur.empty()) { out.push_back(std::move(cur)); cur.clear(); } + } + if (!cur.empty()) out.push_back(std::move(cur)); + return out; +} + +} // namespace + +// Heuristic. Names like GND, GNDA, SHIELD, CHASSIS → GndShield (name alone is +// reliable there — left out of the control-token logic on purpose). Names +// containing PWR/POWER/VCC/VDD/VEE/VSS, or starting with VS_/VBAT/+/− → rail +// candidates; a rail candidate whose tokens include a control word (SENSE, +// EN, PG, …) is downgraded to PowerAdjacent. Else Other. +NameClassification classify_signal_name(const std::string &name) { + NameClassification out; + if (name.empty()) return out; std::string u = name; std::transform(u.begin(), u.end(), u.begin(), [](unsigned char c) { return std::toupper(c); }); @@ -52,14 +110,14 @@ SignalType infer_signal_type(const std::string &name) { return u.rfind(needle, 0) == 0; }; - if (u == "GND" || u == "GROUND") return SignalType::GndShield; - if (starts_with("GND_") + if (u == "GND" || u == "GROUND" + || starts_with("GND_") || (starts_with("GND") && u.size() >= 4 - && std::isalpha((unsigned char)u[3]))) { - return SignalType::GndShield; + && std::isalpha((unsigned char)u[3])) + || contains("SHIELD") || contains("CHASSIS") || contains("EARTH")) { + out.verdict = NameVerdict::GndShield; + return out; } - if (contains("SHIELD") || contains("CHASSIS") || contains("EARTH")) - return SignalType::GndShield; if (contains("PWR") || contains("POWER") || contains("VCC") || contains("VDD") || contains("VEE") || contains("VSS") @@ -67,7 +125,25 @@ SignalType infer_signal_type(const std::string &name) { || starts_with("VS3_") || starts_with("VS4_") || starts_with("VBAT") || starts_with("VBUS") || starts_with("+") || starts_with("-")) { - return SignalType::Power; + for (const std::string &tok : alnum_tokens(u)) { + if (is_power_control_token(tok)) { + out.verdict = NameVerdict::PowerAdjacent; + out.token = tok; + return out; + } + } + out.verdict = NameVerdict::Rail; + return out; + } + return out; +} + +SignalType infer_signal_type(const std::string &name) { + switch (classify_signal_name(name).verdict) { + case NameVerdict::Rail: return SignalType::Power; + case NameVerdict::GndShield: return SignalType::GndShield; + case NameVerdict::PowerAdjacent: + case NameVerdict::Other: break; } return SignalType::Other; } diff --git a/src/frontends/tui/commands.cpp b/src/frontends/tui/commands.cpp index 8dcb49a..c80dab4 100644 --- a/src/frontends/tui/commands.cpp +++ b/src/frontends/tui/commands.cpp @@ -152,7 +152,9 @@ void Tui::RegisterCommands() { Print(" types: " + std::to_string(r.power) + " power, " + std::to_string(r.gnd) + " gnd, " + std::to_string(r.kept_other) - + " suspect Power (name only — kept as Other)"); + + " suspect Power (name only — kept as Other), " + + std::to_string(r.adjacent) + + " power-adjacent (control — kept as Other)"); }, /*prompt_for_missing=*/ true, "load a module from a netlist / pinout file (mentor, altium, ods)", diff --git a/src/frontends/tui/screen_analyze.cpp b/src/frontends/tui/screen_analyze.cpp index 19c755c..c2f6407 100644 --- a/src/frontends/tui/screen_analyze.cpp +++ b/src/frontends/tui/screen_analyze.cpp @@ -131,29 +131,39 @@ Component Tui::BuildAnalyzeScreen() { // ============================================================= Types // Power decisions (confirmed / refuted) and NC orphan breakdown. analyze_types.clear(); - int conf_pwr = 0, ref_pwr = 0, gnd = 0; - struct Row { char kind; std::string mod, sig; int fanout; bool voltage; }; + int conf_pwr = 0, ref_pwr = 0, adj = 0, gnd = 0; + struct Row { char kind; std::string mod, sig; int fanout; bool voltage; + std::string token; }; std::vector rows; for (auto &mkv : *sys->modules()) { Module *mod = mkv.second; for (auto &skv : *mod->signals) { Signal *s = skv.second; - SignalType named = infer_signal_type(s->name); + NameClassification ncl = classify_signal_name(s->name); char kind = 0; - if (named == SignalType::GndShield && s->type == SignalType::GndShield) { + std::string token; + if (ncl.verdict == NameVerdict::GndShield && s->type == SignalType::GndShield) { kind = 'G'; ++gnd; - } else if (named == SignalType::Power && s->type == SignalType::Power) { + } else if (ncl.verdict == NameVerdict::Rail && s->type == SignalType::Power) { kind = 'P'; ++conf_pwr; - } else if (named == SignalType::Power && s->type == SignalType::Other) { + } else if (ncl.verdict == NameVerdict::Rail && s->type == SignalType::Other) { kind = 'R'; ++ref_pwr; + } else if (ncl.verdict == NameVerdict::PowerAdjacent) { + kind = 'A'; ++adj; token = ncl.token; } else continue; rows.push_back({kind, mod->name, s->name, - (int)s->size(), has_voltage_pattern(s->name)}); + (int)s->size(), has_voltage_pattern(s->name), + token}); } } + // Deliberate display order: confirmed rails, then the suspects (the + // actionable residue), then the power-adjacent controls, gnd last. + auto rank = [](char k) { + return k == 'P' ? 0 : k == 'R' ? 1 : k == 'A' ? 2 : 3; + }; std::sort(rows.begin(), rows.end(), - [](const Row &a, const Row &b) { - if (a.kind != b.kind) return a.kind < b.kind; + [&](const Row &a, const Row &b) { + if (a.kind != b.kind) return rank(a.kind) < rank(b.kind); if (a.mod != b.mod) return a.mod < b.mod; return a.sig < b.sig; }); @@ -181,6 +191,10 @@ Component Tui::BuildAnalyzeScreen() { else reason = "name only — fan-out " + std::to_string(r.fanout) + ", no voltage"; + } else if (r.kind == 'A') { + tag = "[Pwr-adjacent] "; + reason = "control token '" + r.token + + "' in name — kept as Other"; } else { tag = "[Gnd] "; reason = "name match (fan-out " + std::to_string(r.fanout) + ")"; @@ -200,7 +214,8 @@ Component Tui::BuildAnalyzeScreen() { std::string types_header = "Types: " + std::to_string(conf_pwr) + " Power, " + std::to_string(ref_pwr) - + " Suspect, " + std::to_string(gnd) + + " Suspect, " + std::to_string(adj) + + " Adjacent, " + std::to_string(gnd) + " Gnd"; // Tab bar — horizontal headers, active one inverted. @@ -240,8 +255,13 @@ Component Tui::BuildAnalyzeScreen() { "Name suggests Power AND structure agrees: fan-out ≥ 4 pins, " "or a voltage pattern in the name (e.g. 3V3, 5V, 12V)."), term("Suspect Power", - "Name suggests Power but the structural check failed — " - "fan-out too low and no voltage in the name."), + "Name suggests Power, no control token explains it, but the " + "structural check failed — fan-out too low and no voltage " + "in the name."), + term("Pwr-adjacent", + "Name holds a rail token AND a control token (SENSE, EN, PG, " + "FB, …): a signal about a rail — measurement or control — " + "not the rail itself. Confidently Other, never suspect."), term("Hard floor", "Fan-out below 3 pins forces Other regardless of the name. " "A real rail physically cannot live on 1-2 pads."), diff --git a/src/frontends/wx/wx_frame.cpp b/src/frontends/wx/wx_frame.cpp index 856819e..4cf067e 100644 --- a/src/frontends/wx/wx_frame.cpp +++ b/src/frontends/wx/wx_frame.cpp @@ -352,9 +352,9 @@ void EssimFrame::OnLoad(wxCommandEvent &) { } Log(wxString::Format( "loaded '%s' from %s — %d part(s), %d signal(s)" - " (dropped %d; types: %d power / %d gnd / %d suspect)", + " (dropped %d; types: %d power / %d gnd / %d suspect / %d pwr-adjacent)", modname, path, r.parts, r.signals, r.dropped, r.power, r.gnd, - r.kept_other)); + r.kept_other, r.adjacent)); RebuildModelView(); } diff --git a/tests/test_analysis.cpp b/tests/test_analysis.cpp index 6a0cbae..4810ccc 100644 --- a/tests/test_analysis.cpp +++ b/tests/test_analysis.cpp @@ -235,7 +235,7 @@ TEST_CASE("infer_signal_types: Power requires name+structural agreement") { Signal *p_3v3 = m->signals->merge("PWR_3V3"); fan_out(p_3v3, 3); // voltage + ≥ floor → Power Signal *vcc = m->signals->merge("VCC"); fan_out(vcc, 5); // fan-out ≥ 4 → Power - Signal *pwr_ok = m->signals->merge("PWR_OK"); fan_out(pwr_ok, 1); // < 3 → hard floor → Other + Signal *pwr_ok = m->signals->merge("PWR_OK"); fan_out(pwr_ok, 1); // control token → adjacent Signal *pwr_2 = m->signals->merge("PWR_2"); fan_out(pwr_2, 2); // < 3 → hard floor → Other Signal *gnd = m->signals->merge("GND"); fan_out(gnd, 1); // gnd: name alone Signal *clk = m->signals->merge("CLK_50MHZ"); fan_out(clk, 3); // not power-ish → Other @@ -243,7 +243,8 @@ TEST_CASE("infer_signal_types: Power requires name+structural agreement") { auto st = infer_signal_types(sys.get()); CHECK(st.power == 2); // PWR_3V3, VCC CHECK(st.gnd == 1); // GND (name alone) - CHECK(st.kept_other == 2); // PWR_OK, PWR_2 below the hard floor + CHECK(st.kept_other == 1); // PWR_2 below the hard floor + CHECK(st.adjacent == 1); // PWR_OK: power-good control, not suspect CHECK(p_3v3->type == SignalType::Power); CHECK(vcc->type == SignalType::Power); @@ -253,6 +254,27 @@ TEST_CASE("infer_signal_types: Power requires name+structural agreement") { CHECK(clk->type == SignalType::Other); } +TEST_CASE("infer_signal_types: power-adjacent beats fan-out — a big sense net is still Other") { + auto sys = std::make_unique(); + Module *m = sys->modules()->merge("M"); + Part *p = new Part("U1"); m->add(p); + + // VDD_CORE_SENSE with fan-out 5: structure alone would confirm Power, + // but the control token settles it as a measurement net → Other, + // counted adjacent (not suspect, not power). + Signal *s = m->signals->merge("VDD_CORE_SENSE"); + for (int i = 0; i < 5; ++i) { + Pin *pin = new Pin("p" + std::to_string(i)); + p->add(pin); s->add(pin); pin->connect(s); + } + + auto st = infer_signal_types(sys.get()); + CHECK(st.power == 0); + CHECK(st.kept_other == 0); + CHECK(st.adjacent == 1); + CHECK(s->type == SignalType::Other); +} + TEST_CASE("infer_signal_types: fan-out hard floor overrides voltage in name") { auto sys = std::make_unique(); Module *m = sys->modules()->merge("M"); diff --git a/tests/test_signal_type.cpp b/tests/test_signal_type.cpp index 0d62e50..8a7ce52 100644 --- a/tests/test_signal_type.cpp +++ b/tests/test_signal_type.cpp @@ -51,7 +51,42 @@ TEST_CASE("infer_signal_type: power family") { CHECK(infer_signal_type("VS3_5V0") == SignalType::Power); CHECK(infer_signal_type("+5V") == SignalType::Power); CHECK(infer_signal_type("-12V") == SignalType::Power); - CHECK(infer_signal_type("VBAT_SENSE") == SignalType::Power); + // Rail token + control token → power-adjacent, mapped to Other by the + // wrapper (it is a sense line ABOUT VBAT, not the rail). + CHECK(infer_signal_type("VBAT_SENSE") == SignalType::Other); +} + +TEST_CASE("classify_signal_name: rail vs power-adjacent control signals") { + CHECK(classify_signal_name("VCC").verdict == NameVerdict::Rail); + CHECK(classify_signal_name("VDD_3V3").verdict == NameVerdict::Rail); + CHECK(classify_signal_name("+5V").verdict == NameVerdict::Rail); + + NameClassification c = classify_signal_name("VDD_CORE_SENSE"); + CHECK(c.verdict == NameVerdict::PowerAdjacent); + CHECK(c.token == "SENSE"); + + CHECK(classify_signal_name("VBAT_SENSE").verdict == NameVerdict::PowerAdjacent); + CHECK(classify_signal_name("VCC_EN").verdict == NameVerdict::PowerAdjacent); + CHECK(classify_signal_name("VCC_EN1").verdict == NameVerdict::PowerAdjacent); // trailing digit + CHECK(classify_signal_name("VDD_FB").verdict == NameVerdict::PowerAdjacent); + CHECK(classify_signal_name("PWR_GOOD").verdict == NameVerdict::PowerAdjacent); + CHECK(classify_signal_name("PWR_OK").verdict == NameVerdict::PowerAdjacent); + CHECK(classify_signal_name("VBUS_DET").verdict == NameVerdict::PowerAdjacent); + CHECK(classify_signal_name("POWER_FAIL").verdict == NameVerdict::PowerAdjacent); + CHECK(classify_signal_name("VDD_VSENSE").verdict == NameVerdict::PowerAdjacent); // fused suffix + CHECK(classify_signal_name("PWR_NFAULT").verdict == NameVerdict::PowerAdjacent); // active-low + + // Whole-token matching: SENSOR is not SENSE, GREEN is not EN — + // these stay genuine rails. + CHECK(classify_signal_name("VDD_SENSOR").verdict == NameVerdict::Rail); + CHECK(classify_signal_name("VCC_GREEN").verdict == NameVerdict::Rail); + + // No rail token at all → Other, even with a control word. + CHECK(classify_signal_name("SPI_CS").verdict == NameVerdict::Other); + CHECK(classify_signal_name("FAN_SENSE").verdict == NameVerdict::Other); + + // GND family is deliberately left out of the control-token logic. + CHECK(classify_signal_name("GND_RET").verdict == NameVerdict::GndShield); } TEST_CASE("infer_signal_type: other (data signals)") {