net.route
the same walk as `net.reaches`, with the route it found bound as a readable value (`VBUS -> [R5] -> VBUS_F -> [L1] -> VDD_3V3`), so a connectivity answer carries the evidence for itself; one route per pair, and a route never ends on a rail because the walk refuses one
What it is ¶
net.route(from, net, path) makes the same walk as net.reaches, and binds the route it found as a
value instead of discarding it. It holds for a pair exactly when net.reaches(from, net) holds, and
path binds a readable rendering of what the walk crossed on the way:
VBUS -> [R5] -> VBUS_F -> [L1] -> VDD_3V3
The names outside the brackets are nets, in crossing order. The name inside each bracket is the series part the walk passed through between them. A net reaches itself at distance zero, so its route is its own name.
The engine could answer whether two points were connected long before it could show how, which meant a reviewer had no way to check the answer (agni issue 518). This relation is the query-side half of the fix. A connectivity answer now carries its own evidence, so a reviewer can read a hundred rows rather than re-ask them one at a time.
For hardware engineers ¶
A resistor, ferrite or fuse in the middle of a signal splits the net without breaking the
connection, so “is this pin joined to that one” is a question about a path across whatever parts sit
in the way. net.reaches answers it. This says which parts those were.
Usually you wanted the parts. “VDD_3V3 is reachable from VBUS” is a fact, and you then go and look it up in the schematic. “VBUS -> [R5] -> VBUS_F -> [L1] -> VDD_3V3” is the same fact with the series bead and the sense resistor named. A reader can decide from it whether that path is the intended one without opening anything.
The walk deliberately leaves out two things. A route never ends on a rail or a plane. The walk refuses a bus-like net outright, because a net every part on the board touches joins everything to everything and proves nothing. A capacitor is never crossed, because it is a DC block rather than a pass element, even though it sits on the net and appears in every connection list.
agni trace is the pin-to-pin form. It does end on a rail, and it reports the test points on each
net of the route. This relation is net-to-net, because a query binds nets.
For software engineers ¶
net.route and net.reaches generate over the same filtered graph. Its nodes are nets, an edge
exists only through a two-net pass element, and bus-like nets are excluded so the traversal cannot
leak into a global singleton. path is a projection of the BFS tree, rendered.
net.route gives one route per pair, not every route. The walk is a breadth-first search and the
path is its tree path, so where two resistors bridge the same two nets the answer names one of them and says nothing
about the other. It is a route and the shortest one, never an enumeration.
path is a string, so every query column is still scalar and a route survives into a csv cell, a
markdown table and a rule’s finding without anything downstream learning a new type. Binding one and
comparing two of them is legal and meaningless. The same is true of the net names in the first two
arguments.
A query cannot tell “no route” from “no such net.” Both are zero rows. That is ordinary datalog,
and it is the right semantics for a rule, since a rule asking about VBUS on a board that has none
should say nothing rather than fail. When you need that distinction, agni trace keeps the three
outcomes apart and exits non-zero on an endpoint that names nothing.
Go projector ¶
None. Like net.reaches, this is computed on demand from the design graph rather than stored, so it
is a datalog predicate (kind predicate in the catalog) rather than an EDB relation with a
projector in stdlib/relations/facts.go. Its generator genRoute (core/query/preds.go) drives
check.Model.Reach and renders each answer with Reach.RouteLine. RouteLine is the fourth
reading of one walk, beside PathTo, ThroughOnPath and StepsTo. genReaches and genRoute
share walk, so the two cannot drift about what is connected.
An empty result means no series path within the walk’s radius, or a from this design does not
have. See the note above about which.
Datalog ¶
Everything a rail reaches, with the route to each:
net.route("VBUS", ?net, ?path) => ?net, ?path
Where a signal ends up and what stands in the way, as a document to save:
net.route("SPI_CS", ?net, ?path) => ?net, ?path
The route to every net carrying a test point, with the test point named. The path column lets a reader check the answer without opening the schematic:
net.route(?from, ?net, ?path), component.net(?ref, ?net), component.class(?ref, "test_point")
=> ?from, ?net, ?ref, ?path
An unbound from walks from every net on the board. agni’s evaluator plans each rule body, so a
walk starts from whatever binds from first, whichever order the body is written in. Before the
planner existed, one shipped rule opened with the walk, and it took agni check from thirteen
seconds to not finishing at all (WS3-114). When nothing in the body binds from, the query asked
for the full walk, and on a large board it costs accordingly. Each walk charges a query’s work budget
for the connections it examines as well as for the rows it emits, so a budgeted full walk stops
partway through instead of after the last start net.
Where this is going ¶
net.reaches and net.route are the same walk asked twice because a path
question still has no way to state its own radius or its own edge class, so each caller hand-codes
one. Issue 374 designs a topology-pattern surface where the radius is a quantifier and the edge
class is a character class, and where a match carries the path it found as a matter of course. If
that ships, both of these become canned patterns over it.