net.signal_level

the signalling level a NON-RAIL net's name declares, the other half of net.nominal_voltage. A house convention that encodes a level into a signal net's name lands here rather than being read as a rail nominal

What it is

net.signal_level(net, volts) yields the voltage a non-rail net’s name declares. It is the other half of net.nominal_voltage: the same name-derived number, over the nets that are not rails.

The two are exhaustive and disjoint over the nets whose names parse. A net carrying the rail role (which includes ground) goes to net.nominal_voltage; every other net whose name carries a voltage token lands here. A name with no parseable token, or with tokens that disagree (12V_TO_5V), yields no row in either relation.

Why the split exists

net.nominal_voltage is documented as a rail’s nominal, and its projector used to emit a row for any net whose name parsed. Many teams encode a signalling level into a signal net’s name, so those levels were landing in a relation that means “rail nominal”. The number was right and the relation carrying it was not.

A verified instance: a net named U3_12_U7_4_3V3 yielded a nominal of 3.3 while classifying as neither rail nor ground. A query asking for rail voltages got a signal net back, and a rule quantifying over rails could not state that it meant rails.

For hardware engineers

A signal net named for its level is telling you what logic family drives it: ..._3V3 is a 3.3 V signal, ..._1V8 is 1.8 V. That is worth knowing, and it is a different claim from “this rail is supposed to sit at 3.3 V”. A level mismatch across a link is an interfacing question (does the receiver tolerate the driver’s level), while a rail nominal out of range is a supply question.

Treat it as the design’s own label, not a measurement. It is exactly as trustworthy as the naming convention that produced it, so the level and the rail nominal are kept apart rather than merged into one number a rule cannot interpret.

For software engineers

The gate is Model.IsRailNet, the narrow role question, which reads the stamped ir.Net.roles fact and falls back to the naming lexicon only for a net that skipped the loader. So the split follows the same role stamp the rest of the engine uses, rather than inventing a second notion of “rail”.

This gates the RELATION only. check.NominalVoltageFromName is a pure string function with no net to ask about, so a Go rule that holds a net and wants rails must gate for itself. The pin-tracking rules are the worked example.

Go projector

netSignalLevelFacts in stdlib/relations/facts.go iterates Model.Nets(), skips any net carrying the rail role, and emits one row per remaining net whose name parses, with Num set to the voltage and a citation to the net’s IR provenance. Netlist-tier: it needs no --params.

Datalog

Every signal net whose name declares a level:

net.signal_level(?net, ?v) => ?net, ?v

Levels that disagree across the design, the cheap way to spot a mixed-voltage interface somebody has to think about:

net.signal_level(?a, ?v1), net.signal_level(?b, ?v2), ?v1 != ?v2 => ?a, ?v1, ?b, ?v2

A signal whose declared level does not match any rail the design carries, which is usually either a naming slip or a level nobody generates:

net.signal_level(?net, ?v), not net.nominal_voltage(?rail, ?v) => ?net, ?v