Files
essim/src/core/domain/signals.cpp
François 1943f1f88a 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>
2026-06-04 16:41:19 +02:00

192 lines
6.8 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "signals.hpp"
#include "parts.hpp"
#include <algorithm>
#include <cctype>
#include <cstring>
#include <vector>
const char *signal_type_name(SignalType t) {
switch (t) {
case SignalType::Power: return "power";
case SignalType::GndShield: return "gnd";
case SignalType::Other: return "other";
}
return "other";
}
SignalType next_signal_type(SignalType t) {
switch (t) {
case SignalType::Power: return SignalType::GndShield;
case SignalType::GndShield: return SignalType::Other;
case SignalType::Other: return SignalType::Power;
}
return SignalType::Other;
}
bool signal_type_from_name(const std::string &s, SignalType &out) {
std::string l = s;
std::transform(l.begin(), l.end(), l.begin(),
[](unsigned char c) { return std::tolower(c); });
if (l == "power" || l == "p") { out = SignalType::Power; return true; }
if (l == "gnd" || l == "g" || l == "shield" || l == "ground")
{ out = SignalType::GndShield; return true; }
if (l == "other" || l == "o" || l == "signal")
{ out = SignalType::Other; return true; }
return false;
}
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); });
auto contains = [&](const char *needle) {
return u.find(needle) != std::string::npos;
};
auto starts_with = [&](const char *needle) {
return u.rfind(needle, 0) == 0;
};
if (u == "GND" || u == "GROUND"
|| starts_with("GND_")
|| (starts_with("GND") && u.size() >= 4
&& std::isalpha((unsigned char)u[3]))
|| contains("SHIELD") || contains("CHASSIS") || contains("EARTH")) {
out.verdict = NameVerdict::GndShield;
return out;
}
if (contains("PWR") || contains("POWER")
|| contains("VCC") || contains("VDD") || contains("VEE") || contains("VSS")
|| starts_with("VS_") || starts_with("VS1_") || starts_with("VS2_")
|| starts_with("VS3_") || starts_with("VS4_")
|| starts_with("VBAT") || starts_with("VBUS")
|| starts_with("+") || starts_with("-")) {
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;
}
Signal::Signal(std::string name)
: SystemElementContainer<Pin>(name), prnt(nullptr),
type(SignalType::Other) {};
void Signal::add(Pin *pin)
{
string pname = pin->prnt->name + "." + pin->name;
SystemElementContainer<Pin>::add(pname, pin);
}
Signals::Signals(void): SystemElementContainer<Signal>("signals") {}
Signals::Signals(std::vector<Signal *> signals): SystemElementContainer<Signal>("signals", signals) {}
Signals::~Signals() {
for (const auto& [key, value] : content) {
delete value;
}
}
int drop_singleton_signals(Signals *signals) {
if (!signals) return 0;
std::vector<Signal *> doomed;
for (auto &kv : *signals)
if (kv.second->size() == 1) doomed.push_back(kv.second);
for (Signal *s : doomed) {
// Detach the lone pin so it surfaces as `(NC)` in views.
for (auto &pkv : *s) {
pkv.second->connect(nullptr);
pkv.second->nc_origin = NcOrigin::DroppedSingleton;
}
signals->remove(s->name);
delete s;
}
return (int)doomed.size();
}
void Signals::add(Signal *signal)
{
SystemElementContainer<Signal>::add(signal);
signal->prnt = this;
}