<a id="parts-and-providers"></a>

# Parts and Providers

When we design a filter, we usually know what each passive must do long before we know which part it will be: 10 kΩ at 1%, or 100 nF rated for at least 10 V. Before the board is ordered, each needs a real part, ideally one your company prefers and keeps in stock. JITX lets the design say only what each passive must do and leaves the choice of part to the project’s configuration. Here we look at how that choice is made, starting from a CSV of your parts and later from your parts database, and at how to check a design before ordering.

<a id="asking-for-a-part-by-what-it-must-do"></a>

## Asking for a part by what it must do

[Resistor](../../jitxlib/jitxlib.parts.components.md#jitxlib.parts.components.Resistor), [Capacitor](../../jitxlib/jitxlib.parts.components.md#jitxlib.parts.components.Capacitor), [PolarizedCapacitor](../../jitxlib/jitxlib.parts.components.md#jitxlib.parts.components.PolarizedCapacitor) and [Inductor](../../jitxlib/jitxlib.parts.components.md#jitxlib.parts.components.Inductor) are ordinary [components](design-hierarchy.md) whose ports are declared but whose part is not yet chosen. They come from the `jitxlib-parts` package, which also supplies everything else on this page, so the project lists it in `dependencies` in `pyproject.toml`:

```toml
[project]
dependencies = [
  # the jitx and jitxlib-standard entries already here, then:
  "jitxlib-parts",
]
```

This [Circuit](../../api/jitx.circuit.md#jitx.circuit.Circuit) asks for two resistors and a capacitor:

```python
class InputFilter(Circuit):
    def __init__(self):
        ...
        self.r = Resistor(resistance=10 * kohm, tolerance=AtMost(1 * pct))
        self.c = Capacitor(capacitance=100 * nF, rated_voltage_dc=AtLeast(10 * V))
        self.pullup = Resistor(resistance=4.7 * kohm)
```

Each set of keywords is a query. Values carry units from [jitx.units](../../api/jitx.units.md#module-jitx.units), and [AtLeast](../../jitxlib/jitxlib.parts.query.md#jitxlib.parts.query.AtLeast) and [AtMost](../../jitxlib/jitxlib.parts.query.md#jitxlib.parts.query.AtMost), from `jitxlib.parts`, turn a value into a bound. Because JITX compares units, `10 * kohm` and `10000 * ohm` are the same query, and a bare `AtLeast(10)` for a voltage raises [DimensionMismatch](../../jitxlib/jitxlib.parts.error.md#jitxlib.parts.error.DimensionMismatch) against a part rated `16 V`. Defaults for the whole design go on the [Design](../../api/jitx.design.md#jitx.design.Design), as [Building the Design](building_the_design.md) shows.

JITX answers the queries together once the whole design has been constructed in Python, or earlier if code reads a part’s `value` or `mpn`.

<a id="providers-and-the-parts-chain"></a>

## Providers and the parts chain

A provider answers queries with real parts: a CSV file of your parts is one, and your parts database can be another. Because the design asks providers instead of naming parts, you can change where parts come from without touching a circuit.

The parts chain is the ordered list of providers a project uses. JITX asks them in order, and each answers a query in one of four ways: it offers matching parts, best first; it reports that nothing matches; it declines because the query uses a key it has no data for; or, as a provider enforcing a company policy might, it refuses with a [Deny](../../jitxlib/jitxlib.parts.provider.md#jitxlib.parts.provider.Deny), which stops the chain and fails the build. The first provider to offer parts wins, and its best part is used. If none does, the build stops with `no part found`. JITX then records each choice in a lockfile, so later builds keep it.

<a id="your-parts-as-a-csv"></a>

## Your parts as a CSV

The quickest provider to start with is a CSV file of your preferred, in-stock passives, much like a list exported from your PLM system. Here is a small one:

```text
category,mpn,manufacturer,resistance,capacitance,case,tolerance,rated_voltage_dc,temperature_coefficient_code
resistor,RC0402FR-0710KL,Yageo,10 kohm,,0402,0.01,,
capacitor,GRM155R71C104KA88D,Murata,,100 nF,0402,0.1,16 V,X7R
```

The project names it in its chain in `pyproject.toml`, with a path relative to that file:

```toml
[tool.jitx.parts]
chain = ["csv:parts/passives.csv"]
```

The [CsvParts](../../jitxlib/jitxlib.parts.providers.csv_parts.md#jitxlib.parts.providers.csv_parts.CsvParts) provider reads the header as what the file can answer. `mpn` and `manufacturer` identify the part, `category` is `resistor`, `capacitor` or `inductor`, and `datasheet` is copied onto the component but not queryable. Every other column some row fills in is an attribute a query can filter or rank on. A cell can carry its unit, as `100 nF` does; a bare number in a value or rating column is read in ohms, farads, henries, volts, watts or amps. A tolerance cell is a fraction, as `0.01` is, or carries `%`, as in `1%`; a bare `1` reads as 100%.

A blank cell means the row lacks that attribute, so it never matches a query that filters on it. A key with no column makes the provider decline: our two-row file has no `max_power` column, so it declines any query for a power rating. A misspelled key, such as `tolerence=`, is passed on as written, so the CSV declines it for want of a column, even though the file holds a 10 kΩ 1% part, and the query goes to the next provider. Of several matching rows, the first in the file wins unless the query ranks them with `rank=`. One case is silent: a path that names no file reads as an empty file, with no message, so every query is declined.

The `jitxexamples` package ships a starter list as the resource `parts/example-passives.csv`, so to use it, add `jitxexamples` to the same `dependencies` list. This copies it into a project, where you can trim it to the parts you use:

```python
from importlib.resources import files
from pathlib import Path

starter = files("jitxexamples") / "parts" / "example-passives.csv"
Path("parts").mkdir(exist_ok=True)
Path("parts/passives.csv").write_text(starter.read_text())
```

<a id="building-before-every-part-is-chosen"></a>

## Building before every part is chosen

With only the CSV in the chain, our 4.7 kΩ pull-up has no row, and the build stops:

```default
my_board.main.Board:
  design: my_board.main.Board
  errors:
    translation failed:
      no part found for resistor(resistance=4.7 kiloohm)
  csv:passives.csv: 0 of 1 resistor rows matched resistor(resistance=4.7 kiloohm)
  hints:
    Relax a filter, or call `explain(query)` to see each provider's reason
```

Early in a design, though, we want to lay out the board before every part is settled. The ideal provider, [IdealPassives](../../jitxlib/jitxlib.parts.providers.ideal.md#jitxlib.parts.providers.ideal.IdealPassives), looks nothing up and makes a part at the requested value. Placed after the CSV, it is asked only when the CSV has no answer:

```toml
[tool.jitx.parts]
chain = ["csv:parts/passives.csv", "ideal"]
```

An ideal part takes the tolerance the query sets exactly, or 1% if it sets none, and its value snaps to the nearest standard value for that tolerance: E24 or E96 at 1%, E24 alone at 5%. Both the snapped value and that tolerance must still match the query:

```python
# 1%, and 31.6 kΩ is an E96 value: IDEAL-R-31600-0402
self.r1 = Resistor(resistance=31.6 * kohm)
# 5%, the nearest E24 value is 33 kΩ, which does not match: no part
self.r2 = Resistor(resistance=31.6 * kohm, tolerance=5 * pct)
# a bound sets no tolerance, and 1% is not within 0.1%: no part
self.r3 = Resistor(resistance=31.6 * kohm, tolerance=AtMost(0.1 * pct))
```

Set exactly, as `tolerance=0.1 * pct`, the same tolerance gets a part, snapped to a finer series. A value given as a span, such as a [Range](../../jitxlib/jitxlib.parts.query.md#jitxlib.parts.query.Range) of 9 kΩ to 11 kΩ, is declined, since there is no single value to make. Besides the value it honors only `case` (0402 if unset), `tolerance` and `polarized`, so a query for 50 V or C0G gets a part that says nothing about either. Ideal parts have part numbers such as `IDEAL-R-4700-0402` and no manufacturer, because there is nothing to buy.

So when the CSV has no answer, the ideal provider supplies a part without any error, as long as it can make the value and a tolerance the query accepts: when no row matches, when the file lacks a column for one of the query’s keys (a misspelled key among them), when it has no rows of that category, and when a `PolarizedCapacitor` meets a file with no `polarized` column (a file marks its polarized capacitors with `true`, `yes`, `y` or `1` in that column). A query it cannot make either, such as the 5% query or the 0.1% bound above, or a span, ends in `no part found`. If the path names no file, every query goes to the ideal provider.

<a id="seeing-which-provider-answered"></a>

## Seeing which provider answered

[explain](../../jitxlib/jitxlib.parts.explore.md#jitxlib.parts.explore.explain) shows what each provider says about one query. It takes a [ResistorQuery](../../jitxlib/jitxlib.parts.query.md#jitxlib.parts.query.ResistorQuery), [CapacitorQuery](../../jitxlib/jitxlib.parts.query.md#jitxlib.parts.query.CapacitorQuery) or [InductorQuery](../../jitxlib/jitxlib.parts.query.md#jitxlib.parts.query.InductorQuery), with the components’ keywords:

```python
from jitx.units import kohm
from jitxlib.parts import ResistorQuery, explain

print(explain(ResistorQuery(resistance=4.7 * kohm)))
```

Run with a working directory inside the project, it reads the project’s chain:

```default
chain from project config
  1 csv:passives.csv         0 of 1 resistor rows matched resistor(resistance=4.7 kiloohm)
  2 ideal                    1 candidate(s)
```

The CSV has every column the query names but no 4.7 kΩ row, so the ideal provider answered. A query the CSV declines for want of a column reads `unsupported:` instead, with the file’s path and `has no such column`; compare the query’s keys with the header, and check the printed path exists. `explain` shows what the chain would answer now, which may differ from what a build already chose.

<a id="setting-the-chain-in-python"></a>

## Setting the chain in Python

A test or a variant board can set the chain in code as a [PartSource](../../jitxlib/jitxlib.parts.provider.md#jitxlib.parts.provider.PartSource), assigned to `parts` on the `Design` or used as a `with` block around some circuits:

```python
from jitxlib.parts import PartSource
from jitxlib.parts.providers import CsvParts, IdealPassives

class Board(Design):
    parts = PartSource([CsvParts("parts/passives.csv"), IdealPassives()])
```

A path given here is resolved against the directory the build starts in. When several settings exist, the closest wins: a `with` block, then the `Design` attribute, then `pyproject.toml`, then a chain published by an installed package, and last, only with `discover = true` under `[tool.jitx.parts]`, one assembled from the installed providers. With none, the build fails with `no parts provider chain configured` ([NoChainConfigured](../../jitxlib/jitxlib.parts.error.md#jitxlib.parts.error.NoChainConfigured)).

Without `source=`, `explain` reads the project’s chain, from `pyproject.toml`, a published chain or discovery, and never a `parts` attribute or a `with` block, so it can describe a different chain from the one your build uses. To explore a chain set in Python, pass it as `source=`:

```python
chain = PartSource([CsvParts("parts/passives.csv"), IdealPassives()])
print(explain(ResistorQuery(resistance=4.7 * kohm), source=chain))
```

The first line of the output names the chain it asked:

```default
chain from explicit source (csv:passives.csv, ideal)
  1 csv:passives.csv         0 of 1 resistor rows matched resistor(resistance=4.7 kiloohm)
  2 ideal                    1 candidate(s)
```

<a id="why-a-build-keeps-its-parts"></a>

## Why a build keeps its parts

Once a part has been placed and routed, we want it to stay put, even as rows are added to the CSV or removed, or the database behind it is updated. If every build chose afresh, any of those could quietly change the board.

The lockfile that holds each choice is `parts.lock.json`, beside `pyproject.toml`. A successful build writes each new choice to it, and so does a [jitx.test.TestCase](../../api/jitx.test.md#jitx.test.TestCase) run; commit it so other machines build with the same parts. A part recorded there is pinned: later builds keep it instead of choosing again. Each entry is keyed on what the query means, the chain, and an optional `lock_scope=` that pins identical queries separately. An entry stays until you remove it or an edited query adopts it, as the next paragraph describes.

Because an entry is keyed on the query and the chain, not on what the CSV holds, editing the CSV moves nothing already chosen, even a part whose row is deleted. The chain, though, is part of each entry’s key, so under a new chain no entry matches and every query resolves again; the old chain’s entries stay, and switching back restores its choices. An edited query misses its own entry, so the build first looks for a leftover entry under the same chain and `lock_scope` whose part still satisfies it. This lets a part already placed survive an edit such as a tightened tolerance: when exactly one leftover entry fits, its part is kept and the entry moves to the edited query; when none fits, or two fit equally well, the query resolves again.

So ideal parts pinned while the CSV path named no file stay pinned after the file is created, because the chain is unchanged. To have parts chosen afresh, delete `parts.lock.json`, or the entries concerned, and build again. `lock-mode` under `[tool.jitx.parts]` selects another [LockMode](../../jitxlib/jitxlib.parts.lock.md#jitxlib.parts.lock.LockMode), such as `off`, which ignores the file.

<a id="checking-before-you-order"></a>

## Checking before you order

Before ordering, two kinds of part need attention: ideal parts, which cannot be bought, and pinned parts your source would no longer give. The parts check finds the second kind, including an ideal part your source can now supply:

```default
jitx design export parts-check my_board.main.Board --fail all
```

Like a build, it needs the project’s JITX runtime running (from a terminal, `jitx runtime start --background`). It resolves every query from scratch, compares the result with what the build used, and with `--fail all` fails on any [FailOn](../../jitxlib/jitxlib.parts.export.md#jitxlib.parts.export.FailOn) category. A design that passes reports:

```default
Parts check: my_board.main.Board
3 pinned
```

This passing design still holds the ideal 4.7 kΩ pull-up, `IDEAL-R-4700-0402`. An ideal part your source still cannot answer passes, because the clean run picks an ideal part for it too. To list these parts, test the design itself: an ideal part’s `mpn` begins with `IDEAL-`, and a [TestCase](../../api/jitx.test.md#jitx.test.TestCase) can walk the design’s [Component](../../api/jitx.component.md#jitx.component.Component) objects with [visit](../../api/jitx.inspect.md#jitx.inspect.visit) and fail on any it finds. Here it is as `test_parts.py`, beside `pyproject.toml`:

```python
from jitx import Component
from jitx.inspect import visit
from jitx.test import TestCase

from my_board.main import Board


class NoIdealParts(TestCase):
    def test_no_ideal_parts(self):
        ideal = [
            f"{trace.path}: {part.mpn}"
            for trace, part in visit(Board(), Component)
            if (part.mpn or "").startswith("IDEAL-")
        ]
        self.assertEqual(ideal, [])
```

Run from the project root with `python -m unittest`, it fails on our design and names where the part sits:

```default
AssertionError: Lists differ: ['circuit.pullup: IDEAL-R-4700-0402'] != []
```

It reads the parts on the design’s own components, under the current chain, so entries an old chain left in the lockfile do not show up.

Because the check compares against the choices in the lockfile, it has nothing to compare with when run before any build, or on a clone without the committed lockfile: it reports `3 unlocked`, one line per query such as `unlocked: resistor(resistance=4.7 kiloohm)`, and fails. Commit `parts.lock.json`, and build before you check.

A part that differs is listed under `differs`, with the file and line that constructed it, what was built and what a clean run would choose. Delete the 10 kΩ row and it reports `built: RC0402FR-0710KL (Yageo)` against `clean run: IDEAL-R-10000-0402`; add a 4.7 kΩ row and the pinned ideal pull-up shows `built: IDEAL-R-4700-0402` against `clean run: RC0402FR-074K7L (Yageo)`.

A build whose chain ends at your own source, without `ideal`, is another way to find ideal parts: it stops with `no part found` at the first part it cannot find, so you fix that part and build again.

To resolve what you find, add the part to your source, change the query to match a part you have, or change the chain so another provider supplies it. A pinned part stays pinned when the source changes, so after adding a row, delete the old entry, then build and check again.

<a id="attaching-your-company-s-parts-database"></a>

<a id="attaching-your-companys-parts-database"></a>

## Attaching your company’s parts database

The CSV gets a project going quickly; in production you add a provider for your company’s own parts database to the chain, and the design code stays the same. [Provider](../../jitxlib/jitxlib.parts.provider.md#jitxlib.parts.provider.Provider) is the contract every provider follows, and `CsvParts` is the worked reference for writing one. A provider package registers under the `jitxlib-parts-provider` entry-point group:

```toml
[project.entry-points.jitxlib-parts-provider]
acme = "acme_parts.provider:AcmeParts"
```

Once it is installed, a chain names it like `csv` and `ideal`:

```toml
[tool.jitx.parts]
chain = ["acme", "csv:parts/passives.csv", "ideal"]
```

The database is asked first; only what it cannot answer reaches the CSV and the ideal provider. Adding it changes the chain, and a changed chain resolves every query again, so parts placed from the CSV can change on the first build after. The parts check cannot warn you beforehand, since under the new chain it reports every query as `unlocked` until that build. So keep the old chain until the design is ready for new parts; its entries stay in the lockfile, and building with it again restores its choices.
