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; a regulator internal is on neither relation

What it is

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

The two are disjoint, and since agni 679 they are not exhaustive. A net with the rail role goes to net.nominal_voltage, and the rail role includes ground. Every other net whose name carries a voltage token goes here. A name with no parseable token, or with tokens that disagree (12V_TO_5V), yields no row in either relation.

A regulator internal is the third case and yields no row either. 12V_FB and 12V_SW are named after the rail they serve rather than after what they carry, so the number is a different net’s voltage and is neither a rail nominal nor a signalling level. If only the rail side dropped them, they would show up here instead, with the same wrong number under a relation that claims less. So both projectors exclude them.

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 showing up in a relation that means “rail nominal”. The number was right and the relation carrying it was not.

In one 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 that no rule can 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 when a Go rule holds a net and wants rails, the rule 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 or classed as a regulator internal (Model.IsRegulatorInternalNet), 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. The relation is netlist-tier and 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. This is the cheap way to spot a mixed-voltage interface that 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 matches no rail in the design. That is usually either a naming slip or a level that nothing generates:

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