Querying your design
Search your whole design as data with ad-hoc datalog queries, each answer traceable to its source.
A check answers “does my design break this rule?” A query answers a question you make up
on the spot: “which nets carry more than 12V?”, “which parts have no datasheet?”, “how many parts
sit on each net?”. agni query lets you search your whole design as data, and every answer comes
back with its source so you can go check it.
The tool turns your design into a set of simple facts. A net has a voltage, a part sits on a net, a datasheet lists a limit, a copper track has a width. A query is a question over those facts.
The facts you can ask about ¶
Each fact is a named relation with a few fields. You query them by name:
| Relation | Reads | From |
|---|---|---|
net.max_voltage(net, volts) |
a net’s rail voltage | the schematic |
component.mpn(ref, mpn) |
a part’s manufacturer part number | the schematic |
component.class(ref, class) |
a device class the part is in (a family tag too: a TVS is both tvs and diode) |
the schematic |
component.net(ref, net) |
a part sits on a net | the schematic |
param.max(mpn, symbol, value) |
a datasheet limit | --params (see Datasheets) |
net.reaches(from, net) |
nets reachable through passives | the connectivity |
net.route(from, net, path) |
the same walk, plus what it crossed to get there | the connectivity |
board.track_width(net, mm) |
a net’s thinnest copper track | the PCB |
board.via_drill(net, mm) |
a net’s smallest via drill | the PCB |
board.layer(net, layer) |
a layer the net is routed on | the PCB |
The datasheet facts need a parameter set (--params), and the board facts need a .kicad_pcb or an
IPC-2581 board. Ask for a fact your design doesn’t carry and you simply get no rows. The tool never
makes one up.
That table is the short form. Every relation also has a full card covering what it means for hardware, how the projection is built, and the cases where an empty result is not a clean answer. This page inlines the cards for the relations it uses at the bottom of this page; the complete set is the relation catalog.
A name before a dot is a module and the part after it is a member: net.pin_count is the
pin_count member of the net module. No name is both, so these relations moved into modules
(agni issue 751). A query saved before the move names them the old way and now gets
“unknown relation”:
| Was | Now |
|---|---|
component-on-net |
component.net |
pin |
component.pin |
param |
param.max |
reaches, route |
net.reaches, net.route |
rail, feedback, switching |
net.rail, net.feedback, net.switching |
external_signal_net |
net.connector_signal |
has_nc_channel, types_power_out, has_netclass, has_netclass_defs |
design. plus the same name |
pin_net_conflict, ref_des_collision, unresolved_symbol |
reader. plus the same name |
contains, prefix, suffix, glob, match |
str. plus the same name |
Writing a query ¶
A query is a list of facts separated by commas, an optional filter, and an optional => that picks
which columns to show:
component.mpn(?ref, ?mpn), net.max_voltage(?net, ?v), ?v < 30 => ?ref, ?net
?ref,?mpn,?net,?vare variables, blanks the tool fills in.- Reusing a variable joins:
?netin two facts means “the same net in both”. ?v < 30filters. Operators:< <= = != > >=. Text in double quotes:"VIN", taken exactly as written, since a string has no escapes. Numbers plain.=> ?ref, ?netpicks the answer columns. Omit it to show every variable.
That is the whole language. The examples below build on it.
A query that does not parse says where it stopped. agni query prints the line with a caret under
the place, and the viewer’s query panel shows the same under its error:
error: invalid argument: query: variable ?r ?n: a variable's name is letters, digits and _
component.net(?r ?n) => ?r
^
The five-rung ladder ¶
The panel offers these starter queries as click-to-run chips, and agni query --examples prints
the same set. Each adds exactly one idea over the one before. Run one, read the result, then
edit it. Every rung names its concept, with a one-line analogy for the SQL-literate.
1. Every part on every net (projection) ¶
component.net(?ref, ?net) => ?ref, ?net
A single fact is already a question. This lists each part and the net it sits on. => picks the
answer columns, SQL’s SELECT list, with the relations as the tables.
2. Rails above 3V (filter) ¶
net.max_voltage(?net, ?v), ?v > 3 => ?net, ?v
A bare comparison prunes rows, a WHERE clause. Operators: < <= = != > >=. Numbers plain, text in
"quotes".
3. Parts sitting on a rail above 3V (join) ¶
component.net(?ref, ?net), net.max_voltage(?net, ?v), ?v > 3 => ?ref, ?net, ?v
Reusing ?net in two facts means “the same net in both”. In SQL that is a JOIN ... ON. A join
connects what a part is, where it sits, and what its rail carries.
The two relations are separate tables of facts until a variable appears in both. That shared ?net
pairs a row on the left with a row on the right, and ?v > 3 then decides which of the paired rows
survive.
4. Parts on USB nets (predicate) ¶
component.net(?ref, ?net), str.contains(?net, "USB") => ?ref, ?net
str.contains is a test over an already-bound value, SQL’s LIKE '%USB%'. str.prefix/str.suffix are the
anchored variants.
5. Reachable through series pass elements (recursion) ¶
net.reaches(?from, ?net) => ?from, ?net
net.reaches walks connectivity transitively through series passives (a resistor, an inductor, a
ferrite bead, a fuse). In SQL it is a recursive CTE / transitive closure over the
connectivity graph. It answers “what does this rail actually feed after the filter”. No per-net
question can see across a series element.
Where these commands run ¶
Every transcript below is generated by running the command shown, so each one works as written when you run it from its sample’s working directory. Three samples appear, all checked into a clone of the engine repo:
| Sample | Run from | What it is |
|---|---|---|
designs/gateway/gateway.edn |
examples/tutorial-project |
the tutorial’s synthetic gateway board, with a team’s naming and parameters wrapped around it |
regulator.fires.kicad_sch, fires.edn |
cmd/agni/testdata/conformance |
small single-purpose boards, each built so one thing has something true to report |
board.kicad_pcb |
readers/kicad/testdata |
a board export, for the copper facts a netlist cannot answer |
Point any of them at your own design instead and the same queries run. Nothing here depends on the sample beyond the names it happens to carry.
Going further ¶
Find something by name ¶
entity(name, kind) names everything that exists. Every other relation ranges over a relationship,
so a search built on one inherits its blind spots. A part that sits on no net has no row in
component.net, so a search through component.net cannot find it.
$ agni query designs/gateway/gateway.edn 'entity(?name, ?kind), str.contains(?name, "CAN") => ?name, ?kind'
name kind provenance
CAN1_CANH net designs/gateway/gateway.edn:CAN1_CANH
CAN1_CANL net designs/gateway/gateway.edn:CAN1_CANL
CAN1_RXD net designs/gateway/gateway.edn:CAN1_RXD
CAN1_TXD net designs/gateway/gateway.edn:CAN1_TXD
4 result(s)
kind is component, net or bus, so you can scope the search:
$ agni query designs/gateway/gateway.edn 'entity(?name, "component"), str.prefix(?name, "U") => ?name'
name provenance
U1 designs/gateway/gateway.edn
U2 designs/gateway/gateway.edn
U3 designs/gateway/gateway.edn
U4 designs/gateway/gateway.edn
U5 designs/gateway/gateway.edn
5 result(s)
One of the string predicates matches the name: str.contains, str.prefix, str.suffix,
str.glob or str.match. str.prefix cannot express a leading wildcard. A leading wildcard finds
every net named for the rail it carries, whichever block named it:
$ agni query designs/gateway/gateway.edn 'entity(?name, "net"), str.glob(?name, "*_1V8") => ?name'
name provenance
PMIC_IO_1V8 designs/gateway/gateway.edn:PMIC_IO_1V8
1 result(s)
str.glob follows SQLite’s GLOB: * is any run of characters (including /, so a hierarchical
name matches), ? is one character, and [...] is a character class, so DATA[0-3] matches
DATA0 to DATA3. That makes a literal bracket special. A bus name such as DATA[1:0] is matched
with the bracket inside a class, DATA[[]1:0]. Before jaala v0.1.8 a [ was literal, so a pattern
written for a bracketed name before then now means something else.
The string functions build a name as well as test one. Each puts its result last, so it reads as an
expression (?l = str.replace(?h, "CANH", "CANL")). They are str.concat, str.replace,
str.lower, str.upper, str.capitalize and str.split. str.concat runs from any two of its
three arguments. Given the whole name and its tail it binds the head, and holds for nothing when the
name does not end that way. That form pairs the two halves of a differential pair by their shared
base name:
$ agni query designs/gateway/gateway.edn 'entity(?h, "net"), str.concat(?base, "H", ?h), str.concat(?base, "L", ?l), entity(?l, "net") => ?base, ?h, ?l'
base h l provenance
CAN1_CAN CAN1_CANH CAN1_CANL designs/gateway/gateway.edn:CAN1_CANH ; designs/gateway/gateway.edn:CAN1_CANL
1 result(s)
str.split gives one row per part, or only the part at an index you bind, where a negative index
counts from the end.
Only this relation reaches two cases worth finding during a review. The first is a part connected to nothing:
$ agni query fires.edn 'entity(?ref, "component"), not component.net(?ref, ?any) => ?ref'
ref provenance
X1 fires.edn
1 result(s)
A net with nothing on it, here against a board that has none:
$ agni query fires.edn 'entity(?net, "net"), not component.net(?any, ?net) => ?net'
no results
That empty answer is the answer. Ask for something the design does not carry and you get no rows rather than an invention. The relation cards spend their space on the same property.
Pins are not in entity, because a pin is named by two things rather than one. Enumerate pins with
component.pin(?ref, ?pin), and use the same relation to find a pin. The viewer’s find-by-name box
searches entity, so it will not turn one up.
The web viewer runs this search for you. The query panel has a Find by name mode. Type part of a
name, and the panel writes the same query into the box with your text bound to its ?pattern. It
runs the query and hands you back the query and the binding. Each hit is clickable, whatever sort of
thing it turns out to be. So a search takes you to the drawing, and it leaves the query in the box
for you to edit into a better question.
Reading a result cell in the viewer ¶
A cell is a link when it names a component, a net or a bus, and clicking it highlights that thing on the drawing. The small chips beside it are the sheets it appears on, one per sheet, and clicking one opens that sheet. They are not a grouping of related names, and they have nothing to do with the cell being a net in particular. Any locatable cell gets them.
AVDD_3V3 [ 04_POWER ] [ 07_SENSOR ] [ 11_MCU ] [ +2 ]
That row says the net AVDD_3V3 is drawn on at least five sheets, three of them named. A
single-sheet design shows no chips at all, because there is nowhere to navigate. The strip caps at
three and counts the rest. Past a handful, the interesting fact is how many sheets a thing spans
rather than which. Clicking the +N expands it.
The table marks whichever entity is on the drawing right now, and it marks the chip for the sheet you are looking at. After a click sends the canvas somewhere, those marks tell you where you are. They follow the viewer rather than your last click here, so if you pick something on the drawing instead, the mark moves to that.
Find parts stressed above their datasheet rating ¶
This joins what the part is (component.mpn), what its datasheet says (param.max), where it sits
(component.net), and the rail’s voltage (net.max_voltage), then keeps only the ones where the
rated maximum is below the rail.
$ agni query regulator.fires.kicad_sch --params ./params/ 'component.mpn(?r,?m), param.max(?m,"VIN",?vmax), component.net(?r,?n), net.max_voltage(?n,?rail), ?vmax < ?rail => ?r, ?m, ?vmax, ?n, ?rail'
r m vmax n rail provenance
U1 LM1117 20 /+24V 24 datasheet "SNOS412Q - FEBRUARY 2000 - REVISED JANUARY 2023" page 4, "7.1 Absolute Maximum Ratings" (hand, confidence 1) ; regulator.fires.kicad_sch
1 result(s)
U1 (an LM1117) sits on a +24V rail, but its datasheet caps VIN at 20V. The provenance column
cites both sides, the schematic and the datasheet page, so you can open each and confirm.
Find undatasheeted parts (negation) ¶
not keeps the rows where a fact is absent. The datasheet join populates component.mpn, so
pass --params to ask for “parts on a net that have no MPN”:
$ agni query regulator.fires.kicad_sch --params ./params/ 'component.net(?r,?n), not component.mpn(?r,?m) => ?r'
r provenance
J1 regulator.fires.kicad_sch
1 result(s)
J1 is placed but carries no part number, so a datasheet can never be matched to it.
A not has to be anchored. At least one variable inside it must also appear in a positive
relation, so the negation is about the row rather than about the design. Above, ?r is the anchor, so
not component.mpn(?r,?m) asks whether THIS part has a part number. The ?m is free on purpose and
means “for any value”.
Drop the anchor and the question changes without looking like it has:
entity(?n,"net"), not component.class(?tp,"test_point") => ?n
?tp appears nowhere else, so that asks whether the design contains no test point AT ALL, and on a
board that has even one it filters away every row. It used to answer “no results”, which reads as a
fact about the board. It is now an error naming the unanchored variable.
To negate a pair of relations, name them first. not takes one relation. That spelling was
reaching for nets with no test point ON THEM, and that question needs both component.net and
component.class to be true of the same part. Define a relation for that and negate it by name:
has_test_point(?n) :- component.net(?tp,?n), component.class(?tp,"test_point");
entity(?n,"net"), not has_test_point(?n) => ?n
Clauses are separated by ;, a clause containing :- defines a relation, and the one clause
without one is the question. This is the general form for any “X with no related Y”. Most coverage
questions take that form: rails with no decoupling, buses with no pull-up, parts with no
protection.
A defined relation is fully derived before anything negates it, so the answer does not depend on the order you wrote the clauses in.
Ask what a net IS (roles) ¶
net.role gives you what the engine worked out about a net from its name. A net can carry more than
one, so the relation yields a row per role:
net.role(?n, ?r) => ?n, ?r
Four of the six roles exist to say named after a rail, and not one. A regulator’s pins are
conventionally named for the supply they serve, so a 12V converter’s nodes are 12V_FB, 12V_SW,
12V_MODE1 and 12V_VDRV, none of which carries 12V. Listing them is one clause:
net.role(?n, ?r), ?r != "rail", ?r != "ground" => ?n, ?r
net.rail(?n) and net.role(?n, "rail") are
different sets. net.rail is a conclusion, true for a net that is asserted-driven or global or a ground
or carries the rail role. net.role is what the naming lexicon stamped. On a board with a few
regulators the second is the larger of the two, and a probe-point or decoupling question almost always
wants the first.
Each role’s vocabulary is config, and you extend it in your conventions.
Ask what the design DECLARED about a net (attributes) ¶
net.role is what the engine inferred. net.attr is what the file said:
net.attr(?n, "external", "true") => ?n
external marks a net whose full connectivity was not read, because it continues onto a sheet the
read did not cover. Most connectivity rules exclude those rather than report a one-pin net that is an
artefact of what was opened. It is the net-side twin of component.attr.
Count parts per net (aggregation) ¶
count, min, max, sum, avg and list summarize. Group by the plain columns. The aggregate
reduces the rest. list answers a list value, written as its JSON text (["R1","R2"]), so a script
reading a cell parses it rather than splitting on spaces:
$ agni query regulator.fires.kicad_sch 'component.net(?r,?n) => ?n, count(?r)'
n count(r) provenance
/+24V 2 regulator.fires.kicad_sch
/GND 1 regulator.fires.kicad_sch
2 result(s)
With no plain column, the answer is always one row. The whole result is then a single group,
and it exists even when nothing matched, so a count of something the design lacks answers 0
rather than “no results”:
net.ground(?n), component.net(?tp,?n), component.class(?tp,"test_point") => count(distinct ?tp)
count and sum over nothing are 0 and list is the empty list []. min, max and avg have no
number to report, so their cells are absent.
A projection that does group, such as => ?n, count(?r), still answers nothing when nothing
matched, because there is no net to name a group after.
So a grouped count never lists the nets with nothing to count. Most reviews want that coverage table
for test points, and the library member net.test_point_count gives every net a row and a 0 where
it has none, so the uncovered nets sort to the top:
$ agni query netlist-audit.tel 'net.test_point_count(?n, ?c) => ?n, ?c order by ?c, ?n'
n c provenance
I2C_SCL 0 netlist-audit.tel
MCU_NRST 0 netlist-audit.tel
GND 1 netlist-audit.tel
I2C_SDA 1 netlist-audit.tel
VCC_3V3 1 netlist-audit.tel
5 result(s)
Keep only some groups (having) ¶
A comparison in the question filters facts, one at a time, before there is any group. To ask about
the group itself you need having, which runs after the reduce. “Nets carrying more than one part”:
$ agni query regulator.fires.kicad_sch 'component.net(?r,?n) => ?n, count(?r) having count(?r) > 1'
n count(r) provenance
/+24V 2 regulator.fires.kicad_sch
1 result(s)
The aggregate does not have to be a column. Drop count(?r) from the projection and keep the
having, and the answer is the nets rather than the tally. A coverage question usually wants the
nets:
component.net(?r,?n) => ?n having count(?r) > 1
Counting values instead of bindings (distinct) ¶
An aggregate reduces one entry per ANSWER, not per distinct value. A question that joins two
things produces one answer per combination, so a part appears once for every partner it was paired
with, and count counts all of them. distinct reduces the values instead:
$ agni query regulator.fires.kicad_sch 'component.net(?r,?n), component.net(?other,?n) => ?n, count(?r), count(distinct ?r), list(distinct ?r)'
n count(r) count(distinct r) list(distinct r) provenance
/+24V 4 2 ["J1","U1"] regulator.fires.kicad_sch
/GND 1 1 ["U1"] regulator.fires.kicad_sch
2 result(s)
+24V carries two parts. Asking twice about what sits on a net pairs each with each, so there are
four answers and count(?r) reports 4. count(distinct ?r) reports the 2 you meant, and
list(distinct ?r) names them.
distinct works the same way on every aggregate, list included, so a projection carrying both
count(?r) and list(?r) always describes the same set. If you are ever unsure which you have,
select both spellings and compare, as above.
A defined relation also gets there. It projects the extra column away before the group forms:
on(?r,?n) :- component.net(?r,?n), component.net(?other,?n);
on(?r,?n) => ?n, count(?r)
The defined relation keeps ?r and drops ?other, so its tuples are already one per part per net
and a plain count matches count(distinct ?r). Drop ?r from the head too and you are counting
nets, not parts. That is the easiest mistake to make with this idiom.
Count inside a clause (body aggregates) ¶
A grouped count leaves out every group with nothing to count. A body aggregate counts inside the
clause instead. The rest of the clause gives the shared variable its values, and the aggregate
counts once for each, so the empty groups answer 0. Every net with the number of test points on it:
entity(?n, "net"), ?c = count(distinct ?tp) : { component.net(?tp, ?n), component.class(?tp, "test_point") } => ?n, ?c
The braces share a variable with the clause (?n here). A relation outside the braces has to bind
it, as well as one inside them. net.test_point_count in the shipped library is this clause.
Keep a row when part of it is missing (optional) ¶
optional { ... } keeps the row whether or not the braces match. Where they do not match, a
variable bound only inside the braces is absent. Every resistor with its part number, and the
resistors that have none still listed:
component.class(?r, "resistor"), optional { component.mpn(?r, ?m) } => ?r, ?m
Without optional, a resistor with no part number would drop out of the answer. That answer reads
the same as a design with no such resistor.
Sort and page an answer (order by, limit) ¶
order by sorts by projected columns, desc reverses one, and limit keeps the first rows. With an
order by, the evaluation holds only the page while it runs, so asking for the ten busiest nets on
a large board does not build the whole answer first:
net.pin_count(?n, ?c) => ?n, ?c order by ?c desc, ?n limit 10
offset skips rows and after continues from the last row of the page before, by value. Both need
an order by.
A limited answer says how many rows it left out, as 10 of 1617 result(s), 1607 left out by the limit under the table and as total in --format json. On the command line --limit 10 caps an
answer without editing the query, and so does limit on the RunQuery request. Where the query
writes its own limit, the smaller applies. Counting the total solves the whole goal, so a limit
saves keeping, locating and sending the rows it leaves out, and does not save the solving.
Units ¶
A voltage carries volts and a board width carries metres, shown in millimetres. When all of a relation’s numbers share a unit, the relation declares it. Comparing two different units is refused before the query runs, since volts are never less than metres:
net.max_voltage(?n, ?v), board.track_width(?n, ?w), ?v < ?w => ?n
A datasheet parameter’s unit varies from row to row, so its relations declare none and each value
keeps its own. An aggregate can mix units, such as sum over one part’s limits in volts and amps.
It then answers absent, and the cell says why (absent: mixed units: A, V). num.value and
num.unit take a number apart when a question really does mean the bare number.
A constant can carry its unit, so ?v >= 3.3V and ?v > 3000mV ask the same thing, since a prefix is
folded into the base unit before anything compares. A constant in the wrong unit is refused the same
way a relation’s is, and ?v > 3mm against a voltage fails with compares number[V] with number[m], which never holds.
Arithmetic ¶
A comparison can compute. +, -, * and / work on numbers, with parentheses and unary minus,
and ?x = expression binds a new variable to the result:
net.max_voltage(?n, ?v), ?mv = ?v * 1000 => ?n, ?mv order by ?mv desc
An expression can sit on either side of any comparison, so net.pin_count(?n, ?c), ?c * 2 > 20 => ?n
needs no variable of its own. Each operator is a function relation underneath (num.mul for *,
and so on), and those run in whichever direction their bound values allow, so ?v = ?i * ?r finds
whichever of the three is missing. For the same reason, it doesn’t matter in which order an
expression is written.
A one-rule relation whose head ends in its result works as a function, called with that last argument left off:
headroom(?v, ?h) :- ?h = 5V - ?v; net.max_voltage(?n, ?v), ?h = headroom(?v), ?h > 0V => ?n, ?h
The same rule still works as a relation, headroom(?v, ?h).
An expression can also stand in the projection, where its column is named by the expression’s own
text without the ?s:
net.max_voltage(?n, ?v) => ?n, ?v * 1000
answers in columns n and v * 1000. And it can stand in a rule’s head. There it computes the value
as ?y = expression in the body would:
margin(?n, 5V - ?v) :- net.max_voltage(?n, ?v); margin(?n, ?m), ?m > 3V => ?n, ?m
A computed number compares as the constant it looks like, so 5V - 3.3V shows as 1.7 and
net.max_voltage(?n, ?v), 5V - ?v = 1.7V finds the 3.3 V nets (fixed in
panyam/jaala#216).
Use the shipped library (derived relations) ¶
Some questions come up often enough that agni ships them as relations of their own, defined in Datalog over the base relations rather than projected from the design in Go. They sit in the same modules as the relations they extend, so a query calls them the same way:
component.probed_one(?part, ?probed, ?unprobed), component.mpn(?part, ?mpn) => ?mpn, ?part, ?unprobed
| Member | What it answers |
|---|---|
net.has_test_point(net) |
a net at least one test point sits on |
component.two_terminal(part, a, b) |
a part on exactly two nets, the nets in name order so each part answers once |
component.probed_both(part) |
a two-terminal part with a test point on both nets, so an in-circuit tester can measure it |
component.probed_one(part, probed, unprobed) |
a two-terminal part probed on one net only, naming the net that is missing a test point |
agni query --relations prints everything a query can call, module by module, with each member’s
argument types. Give it a module to list one (agni query --relations net), or a member to read its
definition:
agni query --relations component.probed_both
component.probed_both(r: component)
derived, defined in module component
A two-terminal part with a test point on both of its nets, so it can be measured in circuit.
defined as
probed_both(?r: component) :- two_terminal(?r, ?a, ?b), net.has_test_point(?a), net.has_test_point(?b)
Each member’s page says what it counts and leaves out, why the question matters on a board, and how
it is defined. The library lives in stdlib/lib, one file per module, and
Adding a library member covers when a question belongs there. If only
one report asks a question, the question belongs in that report’s query instead, as a rule ahead of
its goal or in a query set’s preamble.
A project can carry its own library too, in a lib/ directory beside project.yaml, laid out the
same way. The tutorial project’s lib/house.dl names the PMIC’s rails after that team’s PMIC_
convention, and a query on its board calls them like any other relation. Of the three, one has
no test point:
$ agni query designs/gateway/gateway.edn 'house.pmic_rail(?n), not house.pmic_probe_point(?n) => ?n'
n provenance
PMIC_MAIN_12V0 designs/gateway/gateway.edn:PMIC_MAIN_12V0
1 result(s)
The members exist only inside that project. agni query --relations house --design <design> lists
them with their definitions, and Adding a library member
covers the rules a project library follows.
See how a row was derived (witnesses) ¶
A row’s provenance names the facts behind it, and for a query over library members that is not the
whole reason. PMIC_MAIN_12V0 answers the query above because house.pmic_rail holds for it and
house.pmic_probe_point does not, and neither is a fact the design states. --witnesses asks the
engine to keep how each row was derived and prints it under the row:
$ agni query designs/gateway/gateway.edn 'house.pmic_rail(?n), not house.pmic_probe_point(?n) => ?n' --witnesses
n provenance
PMIC_MAIN_12V0 designs/gateway/gateway.edn:PMIC_MAIN_12V0
house.pmic_rail(PMIC_MAIN_12V0) via house.pmic_rail(?n: net) :- net.rail(?n), str.prefix(?n, "PMIC_")
net.rail(PMIC_MAIN_12V0) [designs/gateway/gateway.edn:PMIC_MAIN_12V0]
str.prefix(PMIC_MAIN_12V0, PMIC_)
not house.pmic_probe_point(PMIC_MAIN_12V0) (absent)
1 result(s)
Each line is one fact, and the lines under it are what proved it. When a member derived a fact, its
line names the member’s rule after via, with each of its body facts filled in, in the order the
rule is written. A fact from the design ends in its citation. A not that held reads
not ... (absent), because it proves that something is missing and so has nothing to cite. A
predicate such as str.prefix gets a line, and a comparison such as ?a < ?b does not, since the
rule beside it already states it.
Witnesses cost nothing unless asked for. The engine keeps a derivation per derived fact only on a
witnessed query, and a query without the flag runs exactly as it did, at the same work. An aggregate
row has none, because a count merges many derivations into one number.
text and html print the tree under its row, markdown after the table under each row’s number,
and json as each row’s witnesses, whose citations index the answer’s sources as a row’s do.
csv refuses the flag, since a csv’s first row has to be its header and every other row a row of the
table. --set asks every query in the set for witnesses. Over the API it is
RunQueryRequest.witnesses (and RunQueriesRequest.witnesses).
A checklist item written as an inline query opens the same way in the viewer’s Review panel. The arrow beside one of its findings asks the item’s query again with the finding’s subject bound and witnesses on, and draws the tree under the finding. The run keeps the checklist it scored, and the query comes from that copy. So an older run asks its own question and not whatever the checklist file says today. A run older than the open design can name a finding that no longer holds, and the panel says so rather than drawing nothing. A finding from a rule, a tag or a profile has no arrow yet (agni issue 1183).
Ask the same question about different things (bindings) ¶
Say a script asks about one part, then another. It should not build a new query string each time.
Write the query once with a variable where the part goes, and bind the variable to a value with
--bind name=value:
$ agni query designs/gateway/gateway.edn 'component.net(?ref, ?net), net.pin_count(?net, ?fanout) => ?net, ?fanout' --bind ref=U1
net fanout provenance
GND 13 designs/gateway/gateway.edn:GND
PMIC_CORE_3V3 8 designs/gateway/gateway.edn:PMIC_CORE_3V3
PMIC_EN 2 designs/gateway/gateway.edn:PMIC_EN
PMIC_MAIN_12V0 4 designs/gateway/gateway.edn:PMIC_MAIN_12V0
PMIC_PG 2 designs/gateway/gateway.edn:PMIC_PG
5 result(s)
A bound variable answers exactly as the same value written into the query as a constant, so the run
above is component.net("U1", ?net), ... without the quoting. Each position declares a type, and a
value is read as that type. A voltage, a count or a datasheet limit is a number, so --bind min=3
and --bind 'min="3"' both compare as 3, and --bind min=abc is refused as not a number. Where
nothing in the query types the variable, as in ?a < ?b alone, a value that parses as a number binds
a number and a quoted one (--bind 'a="3"') binds the text.
--bind repeats, one per variable. Naming a variable the query does not use is an error, so a typo
fails rather than leaving the variable free and widening the question.
Naming one variable twice binds it to a list, and the query answers for every value in it, as if the
goal were joined with a relation holding exactly those values. So
--bind n=I2C_SDA --bind n=I2C_SCL asks about both nets in one answer, an aggregate counts across
both, and the document view states the binding as ?n in ["I2C_SDA", "I2C_SCL"].
A program sends bindings rather than building text, so a value needs no escaping and a name holding
a quote is asked about exactly. Over the API they are RunQueryRequest.bindings, and
the Python client builds them with agni.bindings({"ref": "U1"}), or agni.bindings({"ref": ["U1", "U2"]})
for a list, which travels as QueryValue.list. A viewer link carries a list as its bind value
repeated, once per value, and opens with the list as one chip. The --format markdown and html
views print them under the query, because the query text alone no longer states the whole question.
Ask many questions at once (query sets) ¶
An audit is usually a workbook rather than a single question: which nets have no test point, how
many ground test points there are, which passives a tester can measure. A query set keeps those
questions in one YAML file, with their shared derived relations written once as a preamble,
and agni query --set answers all of them over ONE read of the design:
title: Test point audit
preamble: |
has_tp(?n) :- component.net(?tp, ?n), component.class(?tp, "test_point");
queries:
- name: Net count
query: entity(?net, "net") => count(?net)
- name: Nets with no test point
query: entity(?net, "net"), not has_tp(?net) => ?net
- name: Ground test points
query: net.ground(?net), component.net(?tp, ?net), component.class(?tp, "test_point") => count(distinct ?tp)
$ agni query netlist-audit.tel --set audit.yaml
Test point audit
mount://files/netlist-audit.tel
== Net count ==
count(net) provenance
5 netlist-audit.tel
1 result(s)
== Nets with no test point ==
net provenance
I2C_SCL netlist-audit.tel
MCU_NRST netlist-audit.tel
2 result(s)
== Ground test points ==
count(distinct tp) provenance
1 netlist-audit.tel
1 result(s)
The preamble holds rules only, and each query brings its own goal. A query may also carry a
description, which the rendered report shows, and a bind: map of values for its own variables
(bind: {ref: U1}, or bind: {ref: [U1, U2]} for a list). A bind: value means what --bind
means for one query. YAML types each value, so 3.3 is a number and "3" is text.
--format markdown and --format html write the whole set as one document with a section per
query, and --format json writes the RunQueriesResponse wire message. csv is refused, because one
csv file holds one table.
A query may be unanswerable, because it names a misspelled relation for instance. It is reported
under its own name, and the rest still answer. One that does not parse also names the line and
column in its own text, counted without the preamble. The command then exits non-zero, after writing
everything, so a script sees both the answers and the gap. examples/netlist-audit is a full audit
written this way, and it needs no preamble at all, because the shipped library (above) holds
everything its tables share.
A project keeps its sets in queries/, one per .yaml file beside project.yaml, and --set
takes a set’s name there as well as a file: agni query designs/gateway --set power-review runs the
tutorial project’s queries/power-review.yaml. As with --checklist, a value ending in .yaml or
.yml is a file and any other value is a name the project must keep. In the viewer, the query
panel’s Sets menu lists the same sets and opens one as a tab per query, in the set’s order. Each
tab answers over one read of the design. From a file… does the same for a set file on your
disk. A query the engine cannot answer shows its error in its own tab.
A set holds TABLES. A question with a pass or fail answer belongs in a check or a review checklist, whose verdicts also say what passed and why.
Search the board (any tier, one language) ¶
Board facts query the same way. A length is held in metres and every answer shows it in
millimetres, the unit the board is drawn in, so a threshold over one writes its unit, ?w < 0.3mm.
A bare 0.3 would be read as 0.3 m and every net would pass it (agni issue 1035). “Nets routed
thinner than 0.3 mm”:
$ agni query board.kicad_pcb 'board.track_width(?net,?w), ?w < 0.3mm => ?net, ?w'
net w provenance
SIG 0.25 board net SIG
1 result(s)
And you can join across the schematic, the datasheet, and the board in one question, “a net routed thin that carries a high-current part”:
board.track_width(?net,?w), component.net(?ref,?net), component.mpn(?ref,?mpn), param.max(?mpn,"IOUT",?i), ?w < 0.25mm => ?net, ?ref, ?i
Follow a rail through passives (reaches) ¶
net.reaches(from, net) walks connectivity through series passives (resistors, ferrites, fuses). Use
it to ask “what does this rail actually feed after the filter”. “Everything reachable from GND”:
$ agni query board.kicad_pcb 'net.reaches("GND", ?n) => ?n'
n provenance
GND net.reaches from GND
SIG net.reaches from GND
2 result(s)
See what the walk crossed (route) ¶
net.reaches tells you a net is reachable. It does not tell you what stands between the two, so you
end up opening the schematic to check an answer the tool already knew. net.route(from, net, path) is
the same walk with that half kept. Its path binds the nets in crossing order, with the part crossed
between each pair, so an answer carries its own evidence.
$ agni query board.kicad_pcb 'net.route("GND", ?n, ?path) => ?n, ?path'
n path provenance
GND GND net.route from GND
SIG GND -> [R1] -> SIG net.route from GND
2 result(s)
Read GND -> [R1] -> SIG as: the walk left GND, went through R1, and arrived at SIG. The names outside
the brackets are nets and the one inside is the part. The first row is the reflexive one, since a net
reaches itself at zero crossings and its route is its own name.
It answers with a route and not every route. The walk is breadth-first, so where two resistors
bridge the same two nets you get the shorter one and no mention of the other. And a route never ends
on a rail or a plane, because the walk refuses to enter one at all.
agni trace is the pin-to-pin form. It does end on a rail, and it
reports the test points sitting on each net along the way.
net.reaches and net.route are the walk the rules use, so a query and a rule agree on what is
reachable. For a walk over a relation of your own, the engine’s
graph.closure(rel, from, to, min, max, hops) and graph.paths take the step relation by name and
walk no further than the hops you give them. Every net two resistor crossings or fewer from
CAN1_CANH:
step(?a, ?b) :- component.class(?r, "resistor"), component.net(?r, ?a), component.net(?r, ?b), ?a != ?b;
graph.closure("step", "CAN1_CANH", ?to, 1, 2, ?h) => ?to, ?h
Unlike net.reaches, this walk enters rails, so a rail’s step ends the question only if your step
relation says so.
Asking under your own vocabulary ¶
Some relations report what the engine believes rather than what is in the file. net.rail,
net.feedback, and pin.type are resolved from a vocabulary at the moment the design is read.
That vocabulary is the built-in one unless you say otherwise, and it is built from the names most boards use. On a board that names rails function-first, the built-in vocabulary can return badly wrong answers for your project.
Isolating that takes moving two things aside rather than one. First, the tutorial project declares a
conventions: section in its project.yaml. Naming the design applies it automatically, so the
before-state is otherwise unreachable, and the run below deletes the section from its copy of the
project. Second, a seeded datasheet’s pin functions establish the rail role on their own. With
params/ in place, this board classifies all four rails whatever the naming vocabulary says. Move
both and the question is about NAMING alone:
$ sed -i.orig '/^conventions:/,/^$/d' project.yaml
$ sed -i.orig2 '/^params:/d' project.yaml
$ agni query designs/gateway/gateway.edn 'net.rail(?n) => ?n'
n provenance
GND designs/gateway/gateway.edn:GND
1 result(s)
Leave the project’s vocabulary in place and ask again, with the corpus still aside so the pair differs in exactly one thing:
$ sed -i.orig '/^params:/d' project.yaml
$ agni query designs/gateway/gateway.edn 'net.rail(?n) => ?n'
n provenance
GND designs/gateway/gateway.edn:GND
PMIC_CORE_3V3 designs/gateway/gateway.edn:PMIC_CORE_3V3
PMIC_IO_1V8 designs/gateway/gateway.edn:PMIC_IO_1V8
PMIC_MAIN_12V0 designs/gateway/gateway.edn:PMIC_MAIN_12V0
4 result(s)
Nothing about the design changed. Author a lexicon in this loop. Ask, compare against the rails you know the board has, fix the pattern, and ask again. See Naming conventions.
The datasheet route reaches the same answer with no lexicon at all. The corpus was doing exactly that before these runs left it out. Datasheets covers that route.
Only the config’s lexicon half is used here, since a query runs no rules.
In the viewer ¶
The viewer has a Query panel that runs the same queries against the design you have open. Type a
query, press Run (or ⌘/Ctrl+Enter), and the results appear as a table. Each row has a small toggle
that expands how the row holds, so the citations are out of your way until you want them. When you
open the first row, the panel asks the question again with
witnesses. The row then shows the tree of rules and facts
behind it. Each net and part in the tree links to the drawing, as a result cell does, and a not
that held is marked absent. If nobody opens a row, the query never asks for witnesses.
The panel runs the query on the server, over the open file, so you get the same answers as
agni query without leaving the design. The vocabulary control in the top bar applies here too,
and a query and a check in the same session then answer under the same vocabulary.
A drawer at the panel’s edge holds Examples & relations. It is agni query --relations with a
mouse. It opens on the root of the namespace, and choosing a module (net ›) lists its members,
each marked with its tier (Netlist, Board, Library and so on). Choosing a member shows its typed
signature, which argument types the engine inferred, a library member’s definition and its reference
page, and an Insert button that puts net.has_test_point(?n) into the query at the cursor. The
breadcrumb above walks back up. The drawer lists the open design’s project library beside the
shipped one, as --relations --design does.
Datasheet relations (param.max, param.typ) answer in the viewer when the design’s project has a
params/ corpus or the server was started with --params, the same corpus agni query reads.
A click on the drawing selects what you clicked and describes it in the Properties panel, and leaves the query box alone. Properties lists a few Ask questions for what you selected (“What nets does it touch?”, “Which test points reach its nets?”), and each opens in a query tab of its own, named for the question and the part, so a query you were writing is still in its tab. + opens an empty tab. A search fills the box in the tab you are on. Either way the query’s variables are bound to what you picked or searched for. The values show as chips under the box, one per variable, so the query text does not change, whatever you picked. Edit a chip’s value to ask about something else, or remove it to leave the variable free and widen the question. Editing a bound variable out of the query drops its chip.
A question about one thing you picked asks for its first 50 rows, so a click on a ground net with a
thousand pins draws quickly. The count under the answer then reads 50 of 1229 result(s) with a
show all button that asks the same question again without the limit. A query you write and run
yourself is never cut.
You do not have to memorize the vocabulary. Below the query box the panel lists every relation as a
click-to-insert chip, grouped by kind (Netlist, Board, Datasheet, Predicates, and any extension
relations your deployment adds). Clicking a chip drops its template at your cursor, so
component.net inserts component.net(?ref_des, ?net) ready to wire into the rest of the
query. Hover a chip to see its full signature and a one-line description.
Every answer is checkable ¶
The last column is always provenance, the source of the facts that produced the row: a schematic file, or a datasheet document, page, and table, or a board net. A query never gives you a number you cannot trace back to its source. In the viewer the provenance sits behind each row’s expand toggle instead of in a trailing column.
Taking a view out of the tool ¶
A question you keep asking is a view, and --format takes one out of the tool.
agni query designs/gateway/gateway.edn \
'component.net(?r,?n), component.class(?r,"test_point") => ?n, ?r' \
--format markdown --title "Test point coverage" > tp-coverage.md
Five formats. text is the aligned terminal table and is the default. csv opens in a spreadsheet.
json is for tooling. markdown and html are documents that carry the title, the design, and
the query itself, above the answer.
A table pasted into a ticket is a screenshot, and nobody downstream can re-run it, scope it differently, or disagree with it. A view carries the question that produced it, so it can be checked the way any other claim can.
--server adds a link that asks the question again in the viewer, printed as look at it: on
stderr, as check and trace do. The link carries the query, its --bind values and its --title
rather than an answer, so following it re-asks the question of the design as it is now, and
--server self serves the design from the same process until Ctrl-C. When a query comes from a
project’s set (--set power-review), its link names the set and the query, one link per query. That
keeps the link short however long the query is. A set read from a file is on no server, so its links
carry each query’s text with the preamble in front.
The link opens the design on its first sheet and brings the Query panel forward with the answer in a tab of its own. When the answer has one row, the viewer draws it the way a click on its first part, net or pin would, on that entity’s sheet. A longer answer draws nothing until you click a cell, because the link cannot say which row it meant. A line above the table says the answer is there. A trace link brings the Trace panel forward the same way.
Two of these behaviours are deliberate:
csvcarries no preamble. No title, no query, no count, because its first row has to be the header for a spreadsheet or a script to bind to it. The document formats carry the question; csv carries the table.- An empty result still says something.
csvwrites the header alone, so a consumer can tell a view that matched nothing from a run that produced no file.markdownandhtmlwrite a sentence rather than an empty table, because a table with no rows reads as an omission rather than an answer.
The design is named by its mount URI, never by the path on the machine that ran the query, so a view is safe to commit or mail.
agni query designs/gateway/gateway.edn 'net.rail(?n) => ?n' --format csv > rails.csv
Following a signal across the parts in the way ¶
A query joins facts, and a path is a sequence of unknown length. A relation can hold a path only as
a rendered string. net.route does that between two nets, one route per pair and never ending on a
rail, so it cannot answer pin to pin. “Which nets carry a resistor” is a query and “what does this
pin go through to reach that one” is not.
agni trace is that second question. It walks from one pin to another through the series parts that
split a net without breaking the path, and prints what it crossed.
$ agni trace designs/gateway/gateway.edn --from U3.10 --to U1.5
U3.10 (NRST) --> R3 --> U1.5 (PG)
route
U3.10 (NRST)
net MCU_NRST
cross R3 (resistor) pin 1 --> pin 2
net PMIC_PG
U1.5 (PG)
routed: 1 crossing, 2 nets, searched to a radius of 6
$ agni trace designs/gateway/gateway.edn --from U1.2 --to U3.2
U1.2 (VOUT) --> U3.2 (VDDIO)
route
U1.2 (VOUT)
net PMIC_CORE_3V3
probe: TP1.1
also: C1.1 (capacitor), C2.1 (capacitor), U2.1 (ic), U4.1 (ic), U5.1 (ic)
U3.2 (VDDIO)
routed: 0 crossings, 1 net, searched to a radius of 6
$ agni trace designs/gateway/gateway.edn --from U3.10 --to U1.1
no route: U3.10 (NRST) --> U1.1 (VIN)
U3.10 (NRST) on net MCU_NRST
U1.1 (VIN) on net PMIC_MAIN_12V0
no series path from MCU_NRST to PMIC_MAIN_12V0 within 6 crossings.
The walk crosses resistors, inductors, ferrites and fuses.
A capacitor is a DC block and is never crossed.
A rail or plane may END a route and is never passed through.
That output shows three things.
The part in the middle is R3. It sits between the MCU’s reset pin and the PMIC’s power-good
output, so the two pins are on different nets and no per-net question can see that they are joined.
A series element that splits a net is the ordinary case, not an awkward one. So people often get
“are these connected” wrong by eye.
The output also lists what else sits on each net, probe points first, because a reviewer reading a route is usually working out where to put a probe or which capacitor is in the way. Filtering the output down to the series elements would remove the most useful thing on the line.
A no is an answer. The third trace found nothing, and it says which nets the two pins are actually on, how far it searched, and what it will and will not cross. A capacitor is a DC block, so the walk never crosses one. A route can END on a rail or plane but never passes through one, since a supply joins everything to everything and a route through one would mean nothing.
The command does not guess. If you name a pin the design does not have, it reports an error rather than an empty result. A name spelled wrong in a declaration and two pins that are genuinely unconnected are opposite problems. Reporting the first as the second would send you to look at the board instead of at what you typed.
What a query is not ¶
A query reports and does not judge. It has no notion of pass/fail, and checks handle that. If you find yourself running the same query to catch a recurring problem, that is a sign it should become a rule.
The relations used here ¶
These are the full cards for the relations in the examples above, inlined so you do not have to leave the page. The relation catalog serves the same text. Both are generated from the relation definitions themselves, so they cannot drift from what the engine actually projects.
Read the “absence is not a pass” section on any relation you scope a question by. An empty result means the fact is absent from your design. That is a different statement from “nothing matched”, and the cards say which is which.
net.max_voltage, a net's rail voltage
What it is ¶
net.max_voltage(net, volts) yields one row per net that declares a rail voltage, pairing the
net name with a number. It is emitted only where the design states a voltage. An explicit
max_voltage attribute on the net wins, and otherwise the value is read from the net’s name (a
rail named +5V, 3V3, or 12V0 names its own nominal). A net with neither channel produces no
row, so the relation is a partial map over nets, not a value for every net.
For hardware engineers ¶
The netlist actually knows the working voltage of these supply rails. Most signal nets carry
no voltage claim, so they are simply absent here. During a review you query it to see which rails
the tool can reason about numerically, and to feed a comparison against a part’s ratings, such as a
join against a datasheet absolute maximum. Suppose a rail you expected does not appear. Then the
extractor could not read its name as a voltage, or the name holds two disagreeing tokens like
12V_TO_5V, and the extractor refuses to guess between them.
For software engineers ¶
It resolves a net to a declared constant, with a miss when nothing
declares one. It is a projection over Nets() in which the extractor tries each net’s explicit
attribute first, then the name, and skips the net when both fail. Rows are 1:1 with the nets that
have a declared voltage; an empty result means no net in the design states or names a voltage.
The value is carried as a number, so a query can range or compare on it without re-parsing the
5V text form.
Go projector ¶
netMaxVoltageFacts in stdlib/relations/facts.go walks Model.Nets() and, for each net, calls
check.RailMaxVoltage(n, n.Name) (in core/check/params.go). That helper returns the explicit
max_voltage attribute when present, else the name-derived nominal (nominalVoltageFromName),
and reports ok=false when neither yields a number. The projector emits a row only when ok is
true, filling both the rendered 5V-style value and the numeric field. One row per net that
declares a voltage; empty when no net declares or names one.
Datalog ¶
List every net with a declared voltage:
net.max_voltage(?n, ?v) => ?n, ?v
Find the rails above 3 V and the parts sitting on them:
net.max_voltage(?n, ?v), ?v > 3, component.net(?r, ?n) => ?r, ?n, ?v
component.mpn, a part's manufacturer part number
What it is ¶
component.mpn(ref_des, mpn) yields one row per component that resolves to a manufacturer
part number, the design-side part identity. The mpn value is the exact orderable part
(“BSS138”, “LM1117MPX-3.3”), not a class or a generic value. A component with no resolved
part number produces no row.
For hardware engineers ¶
This is the BOM answer for a reference designator. It names the physical part that R7 or U3 will be built as. Two schematics can be electrically identical and place different parts. Only the part number distinguishes them, and the datasheet checks join against it. You query it to list what a design actually orders. You can also query it to find which components a datasheet-backed rule can reach, since a component with no part number has no datasheet to check against.
For software engineers ¶
The part number is the lockfile entry (see the analogy
guide). component.net and component.pin
describe the graph structure, and component.mpn binds a node to a concrete pinned artifact
(lodash@4.17.21). Rows are 1:1 with components that carry a resolved part number, so it is a
partial projection over Components(). Unresolved components are simply absent. It is the
design half of the datasheet join key; the datasheet half is param.max(mpn, symbol, max), keyed by
the same string.
Go projector ¶
componentMPNFacts in stdlib/relations/facts.go walks Model.Components() and emits a row for each
component where Model.ComponentMPN(ref) is non-empty. ComponentMPN returns the BomLine
part number when a BOM covers the ref-des, else the component’s mpn field, else “”.
It never parses the free-text Value field, so the identity is only ever what the design
declared. The shared ingestion pass classify.StampMPN fills that field. It reads the
component’s part-number attributes first, then its part type, so this one relation reads uniformly
across formats. The part-number attributes are the lexicon’s attribute.mpn keys, and the
built-ins cover OrCAD/Allegro’s Manufacturer_PN and KiCad’s Part Number.
One row per component with a resolved part number. It needs no --params: every model joins the part
numbers the design carries (agni issue 748). An empty result means the design names no part numbers.
It can also mean the design names them under an attribute missing from the attribute.mpn keys, and
a project’s conventions can add that attribute.
Datalog ¶
Every component and its part number:
component.mpn(?r, ?m) => ?r
Join to the datasheet parameters seeded for that part (the components a datasheet rule can reach):
component.mpn(?r, ?m), param.max(?m, ?sym, ?max) => ?r
component.class, a device class the part is in
What it is ¶
component.class(ref_des, class) yields the device classes a part belongs to. It emits ONE ROW PER
TAG in the part’s class set, not a single most-specific class, so a part with a family tag answers
more than once: a TVS diode answers both component.class(D1, "tvs") and component.class(D1, "diode"), an LED answers both led and diode, a ferrite bead answers both ferrite and
inductor, and a thermistor answers both thermistor and resistor. The class string is the
canonical lowercase name (resistor, capacitor, crystal, net_tie, …). An unclassified component
produces no row (no class is guessed).
For hardware engineers ¶
This is the part’s kind, decided at ingestion from its designator and library type, and widened by the datasheet’s own class when one is joined. The family tags matter because a review often asks a family question, not a specific one: “every diode-family part on this signal” should catch the plain diodes, the LEDs, and the TVS clamps, because electrically they are all diodes. Querying the family tag gives you that set without having to enumerate every subtype.
For software engineers ¶
Think of the class set as an interface hierarchy flattened onto each node, where the part carries both
its concrete type and every base type it satisfies. So the relation is 1:many with a
component. Joining on the specific tag (component.class(?r, "tvs")) is instanceof TVS;
joining on a family tag (component.class(?r, "diode")) is instanceof Diode and matches every
subtype. The set is stamped at the read edge, so the relation is a projection over a precomputed
field, not a re-derivation per query. When a component has no rows, the classifier had no
evidence. That is distinct from “classified as none”.
Go projector ¶
componentClassFacts in stdlib/relations/facts.go walks Model.Components() and, for each, emits
one row per class in Model.Classes(ref) (the full device_classes set: the most-specific class
plus its family tags). Model.ComponentClass(ref) returns just the most-specific one; the relation
uses the full set on purpose so family joins work. One row per tag; empty for an unclassified
component, and empty overall for a design the classifier could not tag.
Datalog ¶
Every component and each class tag it carries:
component.class(?r, ?c) => ?c
Match the whole diode family by its family tag (catches plain diodes, LEDs, and TVS clamps) and report which nets they sit on:
component.class(?r, "diode"), component.net(?r, ?n) => ?n
Schematic ¶
component.net, a part sits on a net
What it is ¶
component.net(ref_des, net) yields one row for each place a component connects to a net: the
reference designator and the net name. A component with three pins on three different nets
produces three rows; a net with eight parts on it produces eight. This is the netlist’s core
adjacency. It links components and nets, and every other netlist relation is keyed on one of the
two.
It is the workhorse join. Most multi-relation queries pass through it to get from a component to
its nets or from a net to its components, and the other relations (net.pin_count, net.bus_like,
net.max_voltage, component.class) hang off one end or the other of this edge.
For hardware engineers ¶
This is “what is connected to what,” the raw wire list. On its own a row just says R1 touches
VBUS. Its value is in the joins: which parts share a rail, whether a connector and a clamp sit
on the same signal, what is loaded onto ground. When you want to answer a connectivity question
about a specific net or part, you start here and add relations that qualify one side.
For software engineers ¶
This is the many-to-many edge table between components and nets, the adjacency list of the design
graph (see the analogy guide, where a net is a
shared channel aliasing pins of many instances). A component maps to many nets and a net to many
components, so neither column is unique. It is the natural join key. When a query relates a
component’s properties to a net’s properties, or the reverse, it joins through this relation, the
way you would join two tables through a link table. It is a projection over Nets() and their
connection lists, so rows are 1:1 with connections; it is empty only for a design with no
connections at all.
Go projector ¶
componentOnNetFacts in stdlib/relations/facts.go walks Model.Nets() and, for each net, emits
one row per entry in net.Connections (the component ref as subject, the net name as object).
Cardinality is one row per (component, net) connection, so a part appears once per net it connects to
and a net once per part on it. Empty only when no net carries any connection.
Datalog ¶
Every component-to-net link:
component.net(?r, ?n) => ?r, ?n
Join to component.class to find every diode and the nets it sits on. Most rules build on this
shape, which pulls a property on one entity through the edge to the other:
component.net(?r, ?n), component.class(?r, "diode") => ?r, ?n
reaches, nets reachable through passives
What it is ¶
net.reaches(from, net) is true when net is reachable from from by walking THROUGH series pass
elements: resistors, inductors, ferrite beads, and fuses. It is a transitive-closure predicate.
The protection rules use it to answer “is there a component of class X somewhere on the path
between these two nets?” without hard-coding a topology.
net.reaches(from, net, hops) is the same walk with the distance exposed, so a rule states the
radius its own question needs. hops is an exact count, not a budget, and the next section explains
why that matters before you use it.
Unlike the fact relations, net.reaches is computed on demand from the design graph rather than
stored, so it is a datalog predicate (kind predicate in the catalog). It is the recursive
counterpart to net.bus_like. net.bus_like names the nets the walk refuses to enter, and
net.reaches is the walk itself.
Distance, and the trap in it ¶
hops binds the ACTUAL number of series crossings, reflexive at 0 (a net reaches itself). Because a
datalog argument binds by equality, putting a bare number in that slot means exactly that distance:
net.reaches(?n, ?rn, 2) # exactly 2 crossings, so it SKIPS a part sitting 1 away
net.reaches(?n, ?rn, ?h), ?h <= 2 # within 2 crossings, which is what a protection question means
The first line reads like a radius and is almost never what a query means. Use the comparison form for a radius.
The engine deliberately holds more than one radius, so the radius belongs in the rule. The
query built-in searches the whole neighborhood (topologyReachHops), because a topology question
like “what is connected to what through passives” wants distant answers. The protection guards ask at
check.ProtectionReachHops (2), and the power-entry walk at check.PowerPathReachHops (3), because
“is a clamp electrically adjacent to this pin” wants only near ones. A discharge pushes through every
series element before the clamp conducts, so a TVS six resistors away protects what is downstream of
itself, not the pin. Suppose a protection predicate were composed out of the wide default. It would
credit that distant TVS and report a genuinely unprotected pin as clean. That outcome is a false
pass rather than a missing result.
For hardware engineers ¶
The walk crosses a two-terminal series part. A resistor, bead, or fuse joins exactly two nets, so crossing one follows the signal one hop along its path. The walk stops at anything that is not a point-to-point series node. A capacitor stops it, since a DC block does not pass the signal on. A ground plane, a global rail or any rail-scale fan-out stops it, since a distribution node is not a path. A part with more than two nets stops it too, since a transceiver or connector is not a series element. With this walk, an ESD or input-protection review can ask “does a clamp sit anywhere between this connector pin and the device pin?”, and a resistor or bead in the middle of the path does not break the question.
For software engineers ¶
net.reaches is transitive reachability over a filtered graph whose nodes are nets. An edge
exists only through a two-net pass element. net.bus_like nets are excluded, so the traversal
cannot leak into a global singleton and mark the whole design reachable. It is a bounded BFS, so a
query over it terminates. A hop cap guards pathological depth, and fan-out and finiteness bound the
walk anyway.
Go projector ¶
net.reaches has no stdlib/relations/facts.go projector because it is not a stored fact. The query
engine evaluates it as a built-in in core/query/preds.go (bounded by topologyReachHops),
delegating to check.Model.Reach in core/check/reach.go. It uses the same IsBusLike stop
predicate that net.bus_like exposes. The protection rules run the same walk at a smaller radius
(see Distance above). The third argument binds Reach.Depth. The BFS records that depth as it goes
rather than re-deriving it from the Parent chain, because the chain can disagree where parallel
passes bridge the same two nets. net.reaches is computed, not projected, so it is outside the
per-relation EDB doc requirement. This page documents it as the reference behind the walk.
Datalog ¶
Every net reachable from a starting net, through series parts:
net.reaches("VBUS_IN", ?net) => ?net
The components that sit on those reachable nets (what a protection walk would find):
net.reaches("VBUS_IN", ?net), component.net(?r, ?net) => ?r
The same question at a protection radius, the way a rule scoped like esd-protection asks it,
over a TVS within two series crossings of the net:
net.reaches(?n, ?rn, ?h), ?h <= 2, component.net(?t, ?rn), component.class(?t, "tvs") => ?n, ?t
When a radius is not behaving as expected, ask how far away each reachable net is:
net.reaches("VBUS_IN", ?net, ?hops) => ?net, ?hops
Schematic ¶
Where this is going ¶
A path question has no way to state itself, so each caller hand-codes a walk. The hop-radius trap
above, the per-caller radius constants, and the baked-in edge class are all symptoms of that.
Issue 374 designs a topology-pattern surface where the radius is a quantifier, the edge class is a
character class, and a match carries the path it found. If that ships, net.reaches becomes one
canned pattern and this page becomes the migration note.
route, the same walk, with the path it took
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.
board.track_width, a net's thinnest copper track
What it is ¶
board.track_width(net, mm) yields one row per net that has routed copper, giving the net’s
MINIMUM track width, held in metres and shown in mm. net is the net name (the join key to
ir.Net.name); mm is a number, so a query compares it with a unit (?w < 0.2mm). A net routed
with several widths reports only its thinnest segment, not every segment.
For hardware engineers ¶
The narrowest copper on a net sets its worst-case current-carrying capacity, so a review cares about that number. A power or ground net that necks down to a thin trace somewhere is a current-density risk even if most of its copper is wide. Query this to find nets routed below a width floor, or to sanity-check that a high-current rail never drops under its intended minimum. The value is the physical track width, so you compare it against a real length, written with its unit.
For software engineers ¶
The routed net is a set of track segments, each with a width. board.track_width is an aggregate
projection over that set, reducing it to the minimum width keyed by net. The minimum is the
safety-relevant extreme. Reporting it instead of one row per raw segment gives a compact per-net
answer rather than a full segment dump. Rows are 1:1 with nets that have at least one track; a net
with pads or vias but no routed track contributes no row. The stored unit is nanometres. The
projector converts to metres, since the engine folds a query literal to that SI base, and a
cell is shown in millimetres.
Go projector ¶
boardFacts in stdlib/relations/facts.go walks Model.BoardNets() and calls the helper
minSegmentWidthNm(bn.Segments) for each net. The helper returns the smallest Width across the
net’s track segments. It returns a false ok when the net has no segments, and then no row is
emitted. The
nanometre minimum is converted with nmToMM, shown as millimetres, and emitted as
board.track_width(net, mm) with the numeric value populated in metres for comparison.
The board tier is EMPTY on a netlist-only design. Model.BoardNets() returns nothing unless the
design was loaded with board geometry (check.WithBoard, fed a .kicad_pcb or IPC-2581 board
sidecar). A query sees no error in that case. board.track_width yields zero rows on any design
without board geometry, as the datasheet tier does without --params. So a query returning nothing
does not mean every track is wide. It can mean the design carries no board at all.
Datalog ¶
Every net and its minimum track width:
board.track_width(?n, ?w) => ?n, ?w
Nets whose thinnest track drops below 0.2 mm:
board.track_width(?n, ?w), ?w < 0.2mm => ?n, ?w
Write the threshold with its unit. A width is held in metres and shown in millimetres, so a bare
0.2 would mean 0.2 m and every net would answer.
Both need a board-bearing design (a .kicad_pcb or an IPC-2581 file); on a netlist-only load they
return nothing.