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-on-net(ref, net) |
a part sits on a net | the schematic |
param(mpn, symbol, value) |
a datasheet limit | --params (see Datasheets) |
reaches(from, net) |
nets reachable through passives | the connectivity |
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), 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. The ones this page leans on are inlined at the bottom of this page; the complete set is the relation catalog.
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". Numbers plain.=> ?ref, ?netpicks the answer columns. Omit it to show every variable.
That is the whole language. The examples below build on it.
Start here: the five-rung ladder ¶
These are the starter queries the panel offers 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-on-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-on-net(?ref, ?net), net.max_voltage(?net, ?v), ?v > 3 => ?ref, ?net, ?v
Reusing ?net in two facts means “the same net in both”, that is a JOIN ... ON. Joins are how you
connect 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
is what 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-on-net(?ref, ?net), contains(?net, "USB") => ?ref, ?net
contains is a test over an already-bound value, SQL’s LIKE '%USB%'. prefix/suffix are the
anchored variants.
5. Reachable through series pass elements (recursion) ¶
reaches(?from, ?net) => ?from, ?net
reaches walks connectivity transitively through series passives (a resistor, an inductor, a
ferrite bead, a fuse), a recursive CTE / transitive closure over the connectivity
graph. It answers “what does this rail actually feed after the filter”, which 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, from the working directory its sample lives in. 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) is the relation that names what exists. Every other relation ranges over a
relationship, so a search built on one inherits its blind spots: looking through component-on-net
cannot find a part that sits on no net, because such a part has no row there.
$ agni query designs/gateway/gateway.edn 'entity(?name, ?kind), 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"), 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)
The string predicates (contains, prefix, suffix, glob, match) decide how the name is
matched. A leading wildcard is the case prefix cannot express, which is how you find every net
named for the rail it carries whichever block named it:
$ agni query designs/gateway/gateway.edn 'entity(?name, "net"), glob(?name, "*_1V8") => ?name'
name provenance
PMIC_IO_1V8 designs/gateway/gateway.edn:PMIC_IO_1V8
1 result(s)
The cases only this relation reaches are the ones worth finding during a review. A part connected to nothing:
$ agni query fires.edn 'entity(?ref, "component"), not component-on-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-on-net(?any, ?net) => ?net'
no results
That empty answer is the answer. Asking for something a design does not carry returns no rows rather than an invention, which is the same property the relation cards spend their space on.
Pins are not in entity, because a pin is named by two things rather than one. Enumerate those with
pin(?ref, ?pin), which is also what to reach for when you want to find a pin: the viewer’s find-by-
name box searches entity, so it will not turn one up.
In the web viewer this has a front door. The query panel has a Find by name mode: type part of a name, and it writes the same query into the box, runs it, and hands you back the query. Each hit is clickable, whatever sort of thing it turns out to be, so a search lands you on the drawing and leaves a sentence you can edit into a better question.
Reading a result cell in the viewer ¶
A cell that names a component, a net or a bus is a link: 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, since past a handful the interesting fact is how many sheets a thing
spans rather than which; clicking the +N expands it.
Whichever entity is on the drawing right now is marked in the table, and so is the chip for the sheet you are looking at. That is what tells you where you are after a click sends the canvas somewhere, and it follows the viewer rather than your last click here: pick something on the drawing instead and 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), where it sits
(component-on-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(?m,"VIN",?vmax), component-on-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. “Parts on a net that have no MPN”
(the component.mpn relation is populated from the datasheet join, so pass --params):
$ agni query regulator.fires.kicad_sch --params ./params/ 'component-on-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:
not component.mpn(?r,?m) asks whether THIS part has a part number. The ?m is free on purpose and
means “for any value”, which is what you want.
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, so the question that
spelling was reaching for, nets with no test point ON THEM, needs both component-on-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-on-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 shape for any “X with no related Y”, which is most
of what a coverage question asks: 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.
Count parts per net (aggregation) ¶
count, min, max, sum and list summarize. Group by the plain columns. The aggregate reduces
the rest:
$ agni query regulator.fires.kicad_sch 'component-on-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)
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-on-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, which is usually what a coverage question
wants:
component-on-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-on-net(?r,?n), component-on-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.
The other way to get there is a defined relation, which projects the extra column away before the group forms:
on(?r,?n) :- component-on-net(?r,?n), component-on-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, which is the mistake this idiom is easiest to make.
Search the board (any tier, one language) ¶
Board facts query the same way. “Nets routed thinner than 0.3 mm”:
$ agni query board.kicad_pcb 'board.track_width(?net,?w), ?w < 0.3 => ?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-on-net(?ref,?net), component.mpn(?ref,?mpn), param(?mpn,"IOUT",?i), ?w < 0.25 => ?net, ?ref, ?i
A single query can span copper, connectivity, and datasheets at once, and each answer stays traceable back to the layer and the datasheet page.
Follow a rail through passives (reaches) ¶
reaches(from, net) walks connectivity through series passives (resistors, ferrites, fuses). It is
how you ask “what does this rail actually feed after the filter”. “Everything reachable from GND”:
$ agni query board.kicad_pcb 'reaches("GND", ?n) => ?n'
n provenance
GND reaches from GND
SIG reaches from GND
2 result(s)
See what the walk crossed (route) ¶
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. route(from, net, path) is
the same walk with that half kept: path binds the nets in crossing order with the part crossed
between each pair, so an answer carries the evidence for itself.
$ agni query board.kicad_pcb 'route("GND", ?n, ?path) => ?n, ?path'
n path provenance
GND GND route from GND
SIG GND -> [R1] -> SIG 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. For the pin-to-pin form, which
does end on a rail and reports the test points sitting on each net along the way, use
agni trace.
Asking under your own vocabulary ¶
Some relations do not report what is in the file; they report what the engine believes. rail,
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 anchored on the names most boards use. On a board that names rails function-first, what the built-in vocabulary returns can be badly wrong for your project.
Isolating that takes moving two things aside rather than one, which is itself worth knowing. The
tutorial project declares its conventions.yaml, so naming the design applies it automatically and
the before-state is otherwise unreachable. And a seeded datasheet’s pin functions establish the rail
role on their own, so with params/ in place this board classifies all four rails whatever the
naming vocabulary says. Move both and the question is about NAMING alone:
$ mv conventions.yaml conventions-off.yaml
$ mv params params-off
$ agni query designs/gateway/gateway.edn 'rail(?n) => ?n'
n provenance
GND designs/gateway/gateway.edn:GND
1 result(s)
Pass your own vocabulary and ask again, with the corpus still aside so the pair differs in exactly one thing:
$ mv params params-off
$ agni query designs/gateway/gateway.edn 'rail(?n) => ?n' --conventions conventions.yaml
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. This is the loop to author a lexicon in: ask, compare against the rails you know the board has, fix the pattern, ask again. See Naming conventions.
The datasheet route is the other way to the same answer, and it needs no lexicon at all: that is what the corpus was doing before it was moved aside, and Datasheets is where it lives.
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 its provenance, so the citations stay out of your way until you want them.
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.
Datasheet (param) facts are not yet wired into the viewer. A query over param returns nothing
there, and datasheet joins stay on the CLI for now.
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-on-net inserts component-on-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, so a search you run is one you can verify. In the viewer the provenance rides behind each row’s expand toggle instead of a trailing column.
Taking a view out of the tool ¶
A question you keep asking is a view, and --format is how one leaves the tool.
agni query designs/gateway/gateway.edn \
'component-on-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: they carry the title, the design, and
the query itself, above the answer.
That last part is the point of the flag rather than a decoration. 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.
Two behaviours worth knowing, both 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 'rail(?n) => ?n' --format csv > rails.csv
Following a signal across the parts in the way ¶
A query joins facts. It cannot follow a path, because a path is not a fact: it is a sequence, of unknown length, and there is no relation whose columns can hold one. So “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.
Three things in that output are the point of the command.
The part in the middle. R3 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 splitting a net is the ordinary case, not an awkward one, which is why “are these connected”
is so often answered wrongly by eye.
What else is sitting there. The parts on each net are listed, 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 rail or plane can be where a route ENDS but is never passed through, since a supply joins everything to everything and a route through one would mean nothing.
What the command refuses to do is guess. A pin you name that the design does not have is an error rather than an empty result, because a name spelled wrong in a declaration and two pins that are genuinely unconnected are opposite problems, and reporting the first as the second sends you to look at the board instead of at what you typed.
What a query is not ¶
A query reports, it does not judge. It has no notion of pass/fail, that is what checks are for. If you find yourself running the same query to catch a recurring problem, that is a sign it should become a rule.
Reference: the relations used here ¶
These are the full cards for the relations the examples above use, inlined so you do not have to leave the page. They are the same text the relation catalog serves, and they 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, which 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 ¶
These are the supply rails whose working voltage the netlist actually knows. 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:
net.max_voltage is the design-side number the supply-exceeds-abs-max join checks against a
datasheet absolute maximum. A rail you expected to appear but does not has a name the extractor
could not read as a voltage (or two disagreeing tokens like 12V_TO_5V, which it refuses to
guess between).
For software engineers ¶
Think of it as a lookup that resolves a net to a declared constant, with a miss when nothing
declares one. It is a projection over Nets(): for each net the extractor tries the 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 check/facts.go walks Model.Nets() and, for each net, calls
railMaxVoltage(n, n.Name) (in 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-on-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: which physical part R7 or U3 will be built as. Two schematics can be electrically identical and place different parts; only the part number distinguishes them, and it is what the datasheet checks join against. You query it to list what a design actually orders, or to find the components a datasheet-backed rule can reach (a component with no part number has no datasheet to check against).
For software engineers ¶
The part number is the lockfile entry (see ANALOGY.md): component-on-net and pin describe
the graph structure, 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(mpn, symbol, max), keyed
by the same string.
Go projector ¶
componentMPNFacts in check/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/mpn attribute, else “”.
It never parses the free-text Value field, so the identity is only ever what the design
declared. Each reader normalizes its own vendor part-number property into the canonical MPN
attribute (OrCAD/Allegro carry it under Manufacturer_PN, which the EDIF reader maps to MPN),
so this one relation reads uniformly across formats.
One row per component with a resolved part number. Empty result: the model was built without a
params tier. The MPN index is populated only by NewModelWithParams, so the relation is empty
unless agni was run with --params (an empty params directory is enough to build the index).
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(?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, …). An unclassified component produces no row (no class is guessed).
For hardware engineers ¶
This is the part’s kind, decided once at ingestion from its designator and library type. 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: the part carries both
its concrete type and every base type it satisfies. That is why 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. Empty rows for a component mean the classifier had no
evidence, distinct from “classified as none”.
Go projector ¶
componentClassFacts in check/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-on-net(?r, ?n) => ?n
Schematic ¶
component-on-net, a part sits on a net
What it is ¶
component-on-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, the link between the two entities every other netlist relation is keyed on.
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 ANALOGY.md: 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: any query relating a component’s properties to a net’s properties (or vice versa) joins
through it, 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 check/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 lands on and a net
once per part on it. Empty only when no net carries any connection.
Datalog ¶
Every component-to-net link:
component-on-net(?r, ?n) => ?r, ?n
Join to component.class to find every diode and the nets it sits on (the shape most rules build
on, a property on one entity pulled through the edge to the other):
component-on-net(?r, ?n), component.class(?r, "diode") => ?r, ?n
reaches, nets reachable through passives
What it is ¶
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 the transitive-closure predicate
the protection rules use to answer “is there a component of class X somewhere on the path between
these two nets?” without hard-coding a topology.
reaches(from, net, hops) is the same walk with the distance exposed, so a rule states the radius
its own question needs. Read the next section before using it: hops is an exact count, not a
budget.
Unlike the fact relations, reaches is computed on demand from the design graph rather than stored, so it is a datalog predicate (kind predicate in the catalog), the recursive
counterpart to net.bus_like: net.bus_like names the nets the walk refuses to enter, and
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:
reaches(?n, ?rn, 2) # exactly 2 crossings — SKIPS a part sitting 1 away
reaches(?n, ?rn, ?h), ?h <= 2 # within 2 crossings — what a protection question means
The first line is the spelling most people reach for and it is almost never what they want. Use the comparison form for a radius.
Why the radius belongs in the rule at all: the engine holds more than one of them, deliberately. 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. Composing a protection predicate out of the wide default would credit that
distant TVS and report a genuinely unprotected pin as clean, which 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 it is following the signal one hop along its path) and stops at anything that is not a point-to-point series node: a capacitor (a DC block, the signal does not continue through it), a ground plane or global rail or any rail-scale fan-out (a distribution node, not a path), and any part with more than two nets (a transceiver or connector, not a series element). This is how an ESD or input-protection review asks “does a clamp sit anywhere between this connector pin and the device pin?” while a resistor or bead in the middle of the path does not break the question.
For software engineers ¶
reaches is transitive reachability over a filtered graph: the nodes are nets, an edge exists only
through a two-net pass element, and 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 (a hop cap guards
pathological depth; fan-out and finiteness bound it anyway), so a query over it terminates.
Go projector ¶
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 and the same IsBusLike stop predicate
that net.bus_like exposes. It is the same walk the protection rules run, at a wider radius (see
Distance above): same traversal, different question. The distance the third argument binds is
Reach.Depth, recorded by the BFS as it goes rather than re-derived from the Parent chain, which
can disagree where parallel passes bridge the same two nets. Because it is computed, not projected,
it is outside the per-relation EDB doc requirement and is documented here as the reference behind
the walk.
Datalog ¶
Every net reachable from a starting net, through series parts:
reaches("VBUS_IN", ?net) => ?net
The components that sit on those reachable nets (what a protection walk would find):
reaches("VBUS_IN", ?net), component-on-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:
reaches(?n, ?rn, ?h), ?h <= 2, component-on-net(?t, ?rn), component.class(?t, "tvs") => ?n, ?t
How far away each reachable net is, the query to run when a radius is not behaving as expected:
reaches("VBUS_IN", ?net, ?hops) => ?net, ?hops
Schematic ¶
Where this is going ¶
The hop-radius trap above, the per-caller radius constants, and the baked-in edge class are all
symptoms of the same thing: a path question has no way to state itself, so each caller hand-codes a
walk. 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 lands, reaches becomes one
canned pattern and this page becomes the migration note.
route, the same walk, with the path it took
What it is ¶
route(from, net, path) is the walk reaches makes, with the route it found bound as a value
instead of discarded. It holds for the same pairs reaches(from, net) holds for, and path binds a
readable rendering of what the walk crossed to get there:
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 is the query-side half of that: a connectivity answer that carries its own evidence, so a hundred rows can be read rather than re-asked 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. reaches answers it. This says which parts those were.
That is usually the part you actually wanted. “VDD_3V3 is reachable from VBUS” is a fact you then go and look up in the schematic; “VBUS -> [R5] -> VBUS_F -> [L1] -> VDD_3V3” is the same fact with the series bead and the sense resistor named, which is enough to decide whether that path is the intended one without opening anything.
Two things it deliberately does not do. 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.
For the pin-to-pin form, which does end on a rail and reports the test points sitting on each net of
the route, use agni trace instead. This relation is net-to-net, because that is what a query binds.
For software engineers ¶
A generator over the same filtered graph reaches walks: 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.
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 stays 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, which is equally true of the net names in the first
two arguments.
A query cannot tell “no route” from “no such net.” Both are zero rows, which is ordinary datalog
and is the right semantics for a rule (a rule asking about VBUS on a board that has none should
stay silent, not fail). When that distinction is the thing you need, agni trace keeps the three
outcomes apart and exits non-zero on an endpoint that names nothing.
Go projector ¶
None. Like 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. The evaluator’s extendRoute (core/query/preds.go) drives
check.Model.Reach and renders each answer with Reach.RouteLine, which is the fourth reading of
one walk beside PathTo, ThroughOnPath and StepsTo. extendReaches and extendRoute share
extendWalk, 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:
route("VBUS", ?net, ?path) => ?net, ?path
Where a signal ends up and what stands in the way, as a document to save:
route("SPI_CS", ?net, ?path) => ?net, ?path
The route to every net that reaches a regulator’s output, joined to the parts sitting there. The path column is what makes the answer checkable without opening the schematic:
route(?from, ?net, ?path), component-on-net(?ref, ?net), component.class(?ref, "test_point")
=> ?from, ?net, ?ref, ?path
Lead with a bound or constant from wherever you can. An unbound first argument walks from every
net on the board, which is the shape GeneratorFirstRules reports and which took one shipped rule
from thirteen seconds to not finishing at all.
Where this is going ¶
reaches and route are the same walk asked twice, which is a symptom rather than a design: 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 lands, 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 in millimetres. net is the net name (the join key to ir.Net.name); mm
is a number, so a query can compare it directly (?w < 0.2). 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 that is the number a review cares about. 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 the threshold you compare against is a real millimetre figure.
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: a reduce to the minimum width, keyed by net. Reporting the minimum (the
safety-relevant extreme) rather than one row per raw segment keeps the relation 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 millimetres so a query reads a natural threshold.
Go projector ¶
boardFacts in check/facts.go walks Model.BoardNets() and, for each net, calls the helper
minSegmentWidthNm(bn.Segments), which returns the smallest Width across the net’s track
segments (and a false ok when the net has no segments, in which case no row is emitted). The
nanometre minimum is converted with nmToMM and emitted as board.track_width(net, mm) with the
numeric value populated for comparison.
The board tier is EMPTY on a netlist-only design. Model.BoardNets() returns nothing unless the
design was loaded with board geometry (NewModelWithBoard, fed a .kicad_pcb or IPC-2581 board
sidecar). For a query this is silent-by-construction: board.track_width yields zero rows on any
design without board geometry, the same posture the datasheet tier takes without --params. 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.2 => ?n, ?w
Both need a board-bearing design (a .kicad_pcb or an IPC-2581 file); on a netlist-only load they
return nothing.