Power inference: classify rail+control names as power-adjacent, not suspect

Names holding both a rail token (VCC/VDD/PWR/...) and a control token
(SENSE, EN, PG, FB, OK, FAULT, ...) are signals ABOUT a rail - feedback,
enable, power-good - so their non-Power classification is confident.
They used to land in the Suspect bucket, drowning the genuine ambiguities.

- classify_signal_name(): 3-state name verdict (Rail / PowerAdjacent /
  GndShield / Other) with whole-token matching (trailing digits stripped,
  long lexemes also match as suffix: VSENSE, PWRGOOD, NFAULT).
  infer_signal_type() becomes a thin wrapper, so the dashboard suspect
  count and the export suspect column shrink automatically.
- infer_signal_types(): PowerAdjacent -> Other + new `adjacent` stat,
  before the structural gate (a big-fanout sense net stays Other).
- LoadResult.adjacent rendered by all three consumers (TUI command,
  script engine, wx log) - outputs kept in sync.
- analyze Types tab: new [Pwr-adjacent] rows with the deciding token,
  deliberate sort order (Power, Suspect, Adjacent, Gnd), glossary entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 16:41:19 +02:00
parent c2b1f4c4ae
commit 1943f1f88a
12 changed files with 219 additions and 34 deletions

View File

@@ -4,6 +4,7 @@
#include <algorithm>
#include <cctype>
#include <cstring>
#include <vector>
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<std::string> alnum_tokens(const std::string &u) {
std::vector<std::string> 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;
}