query module#

Queries and predicates#

A query says what a part has to do. ResistorQuery, CapacitorQuery and InductorQuery carry the typed fields for their category; the category components take the same keywords, so a query object is only needed to share one.

Field values are predicates – Exactly, OneOf, Range with its AtLeast() / AtMost() shorthands, Tolerance, Exists – and bare values and tuples coerce, so the simple cases stay simple:

Resistor(resistance=10 * kohm, case=("0402", "0603"))
Resistor(resistance=Range(9 * kohm, 11 * kohm), tolerance=AtMost(0.01))

rank= orders the matches rather than filtering them, applied lexicographically, and a bare key name means Minimize(). A keyword the category does not name lands in extras and reaches providers as written, so a provider-specific attribute needs no ceremony – in the constructor as much as in PartQuery.update(), which returns a modified copy.

A query is an immutable, hashable jitx.context.Context, so one assigned in a Design or Circuit class body supplies the defaults for everything constructed below it:

class Board(Design):
    shared = ResistorQuery(case="0402", aec_q200=True)

That is the spelling the untyped keys mostly serve: a shared query carries house policy, while the value of a part is per instance.

Dimensioned values are PlainQuantity everywhere and keep their units all the way to the provider. Only PartQuery.digest() reduces them, so that 10 * kohm and 10000 * ohm are one query and share one lockfile entry.

match(), satisfies() and rank_by() are where predicate-against-value semantics are defined. A provider uses them rather than reimplementing them, which is why they are re-exported from jitxlib.parts.provider.

is_quantity(value)[source]#

Narrowing helper. PlainQuantity is generic, so a bare isinstance does not narrow for a type checker.

Return type:

TypeGuard[PlainQuantity]

Parameters:

value (object)

class Predicate[source]#

Bases: object

Base for query value constraints. Use one of the subclasses below, or pass a bare value or tuple and let coerce() pick one.

class Exactly(value)[source]#

Bases: Predicate

Matches a value equal to value.

Parameters:

value (float | int | str | bool | PlainQuantity)

value: float | int | str | bool | PlainQuantity#
class OneOf(values)[source]#

Bases: Predicate

Matches a value equal to any member of values.

Parameters:

values (tuple[float | int | str | bool | PlainQuantity, ...])

values: tuple[float | int | str | bool | PlainQuantity, ...]#
class Range(min=None, max=None)[source]#

Bases: Predicate

Closed interval; either bound may be None for open-ended.

Bounds are quantities for dimensioned attributes, plain numbers for dimensionless ones, and must not be mixed within one Range.

Parameters:
min: float | int | str | bool | PlainQuantity | None = None#
max: float | int | str | bool | PlainQuantity | None = None#
AtLeast(v)[source]#

Range with only a lower bound.

Return type:

Range

Parameters:

v (float | int | str | bool | PlainQuantity)

AtMost(v)[source]#

Range with only an upper bound.

Return type:

Range

Parameters:

v (float | int | str | bool | PlainQuantity)

class Tolerance(typ, plus, minus)[source]#

Bases: Predicate

Typical value with asymmetric bounds, mirroring jitx.Toleranced.

plus and minus are increments from typ to the maximum and minimum, not relative fractions, and None means unbounded on that side. Unlike Toleranced, all three may be quantities.

>>> Tolerance(10 * kohm, 500 * ohm, 500 * ohm)     # 10k +/- 500R
>>> Tolerance.percent(10 * kohm, 5)                # 10k +/- 5%
>>> Tolerance(10 * kohm, None, 500 * ohm)          # unbounded above
Parameters:
  • typ (float | int | PlainQuantity)

  • plus (float | int | PlainQuantity | None)

  • minus (float | int | PlainQuantity | None)

typ: float | int | PlainQuantity#
plus: float | int | PlainQuantity | None#
minus: float | int | PlainQuantity | None#
classmethod percent(typ, plus, minus=None)[source]#

Relative form, asymmetric like Toleranced.percent.

minus defaults to plus. The percentages are dimensionless whatever typ is.

Return type:

Tolerance

Parameters:
class Exists(present=True)[source]#

Bases: Predicate

Matches on presence (or, with present=False, absence) of the key.

Parameters:

present (bool)

present: bool = True#
coerce(value)[source]#

Turn a bare value into a predicate.

Scalars become Exactly, tuples and lists become OneOf, and a predicate passes through unchanged.

Return type:

Predicate

Parameters:

value (object)

class Rank(key, direction='min')[source]#

Bases: object

Order results by key. Not a filter; never excludes a part.

Parameters:
  • key (str)

  • direction ('min' | 'max')

key: str#
direction: 'min' | 'max' = 'min'#
Minimize(key)[source]#

Rank ascending by key.

Return type:

Rank

Parameters:

key (str)

Maximize(key)[source]#

Rank descending by key.

Return type:

Rank

Parameters:

key (str)

type Filter = Predicate | float | int | str | bool | PlainQuantity | tuple[float | int | str | bool | PlainQuantity, ...] | list[float | int | str | bool | PlainQuantity]#
type RankSpec = Rank | str | tuple[Rank | str, ...] | list[Rank | str]#
type ExtrasSpec = Mapping[str, Filter] | tuple[tuple[str, Filter], ...]#
satisfies(predicate, value, key='')[source]#

Whether one value satisfies one predicate.

predicate of None is no constraint and always matches. Presence of the key is the caller’s business, except for Exists, which is about exactly that; see match().

Return type:

bool

Parameters:
satisfies_or_true(predicate, value, key='')[source]#

satisfies, but a None predicate is no constraint.

What a generator provider wants: it synthesized a value and needs to know whether the query would have accepted it, with unconstrained keys passing.

Return type:

bool

Parameters:
match(filters, attributes, identity=None)[source]#

Whether a candidate satisfies every filter.

Identity keys (mpn, manufacturer, category) are read from identity rather than from attributes, since they are fields of PartIdentity; a filter on one of them with identity=None does not match.

A missing key never matches, except Exists(False). A dimensionality mismatch raises DimensionMismatch rather than returning False, because it is a bug and not a non-match.

Return type:

bool

Parameters:
rank_by(items, ranks, key)[source]#

Order items by ranks, applied lexicographically.

key reaches the attribute mapping for an item, since callers rank things that are not themselves attribute maps. Items missing a ranked key sort last, whichever direction is asked for: absent is not better than known.

Return type:

list[T]

Parameters:
round_significant(value, digits=12)[source]#

Round to digits significant digits, to shed float representation noise.

Two callers, same purpose. The digest needs it so that two spellings of one value hash alike: 100 * nF and 1e-7 * F have base-unit magnitudes that differ in the last bits. A provider rescaling a value for display needs it because the rescale has the same problem in reverse: 1e-7 F converts to 99.99999999999999 nanofarad, which is not what anyone wants on a schematic.

Return type:

float

Parameters:
class CanonicalQuery(category, value_key, filters, rank, limit, digest, count=1)[source]#

Bases: object

The flat, category-tagged form that crosses the provider boundary.

Quantities keep their units here; only digest reduces them. lock_scope is deliberately absent, since it has no bearing on which parts match.

Parameters:
category: str#
value_key: str | None#
filters: Mapping[str, Predicate]#
rank: tuple[Rank, ...]#
limit: int#
digest: str#
count: int = 1#

How many components in this batch asked this exact question.

Demand, not a constraint: it says nothing about which parts match, takes no part in the digest, and a provider that ignores it is correct. It is here for the provider that cannot do its job without it – minimum order quantities, bulk rebates, consolidating a reel – which are properties of the order rather than of one query, and are not expressible from a batch that has had its duplicates collapsed.

Two caveats, both structural. Batching is best effort, so an early flush splits one order into several batches and no batch can be assumed to be the whole design. And a query the lock answers never reaches a provider at all, so on a run with a populated lock the batch is the unpinned remainder, not the order – except in check’s clean-run call, which carries every query the design asked precisely so the comparison is against a whole-order resolution. An optimization whose answer depends on the whole order is therefore in tension with pinning by nature: adding one component could legitimately move every selection, which is what a lock exists to prevent. A provider that makes that trade owns it.

describe()[source]#

One-line rendering, for diagnostics and lockfile display strings.

Return type:

str

class PartQuery(**kwargs)[source]#

Bases: Context

Base query. Subclasses add typed fields for their category.

Usable as a design-context default, but never resolvable on its own: it has no value_key, so no category component will accept it.

The constructor takes what update takes: a keyword the category does not name lands in extras rather than raising.

category: ClassVar[str] = 'part'#
value_key: ClassVar[str | None] = None#
mpn: Final[Filter | None] = None#

Manufacturer part number. Filtering on it asks for one specific part.

manufacturer: Final[Filter | None] = None#

Manufacturer name, as the provider spells it.

case: Final[Filter | None] = None#

Package code, such as “0402” or “SOT-23-5”.

extras: Final[tuple[tuple[str, Filter], ...]] = ()#

Filters on attributes the core does not name, such as lcsc or aec_q200.

Pass them as loose keywords or as a mapping; this pairs form is what they are stored as.

rank: Final[RankSpec] = ()#

How to order the matches, applied lexicographically.

A bare key minimizes, so rank=”price” means Minimize(“price”).

lock_scope: Final[str] = ''#

Pins otherwise identical queries to separate lockfile entries.

classmethod from_query(query)[source]#

Build a category query from a more general one.

Nothing is dropped: base fields carry over, extras carries over, and a field that is not typed on the target lands in extras rather than being discarded. An unserved key then surfaces at resolution time with the aggregated diagnostic, which is the same treatment a typo gets.

Return type:

Self

Parameters:

query (PartQuery)

classmethod resolve_context(query=None, **kwargs)[source]#

The Resistor(query=None, **kwargs) path.

Takes the explicit query if given, else the active one from the design context, else an empty one; then applies kwargs.

Return type:

Self

Parameters:
update(**kwargs)[source]#

Return a copy with kwargs applied.

Unknown keywords land in extras; every other field is replaced, never merged, rank included.

Return type:

Self

Parameters:

kwargs (object)

ranks()[source]#

The rank tuple, narrowed. Idempotent: construction already coerced.

Return type:

tuple[Rank, ...]

filters()[source]#

Typed fields and extras, merged and sorted by key.

Return type:

dict[str, Predicate]

canonical(limit=1)[source]#

Flatten to the form a provider sees. Units are kept.

Return type:

CanonicalQuery

Parameters:

limit (int)

digest()[source]#

Stable hash over category, value_key, filters and rank.

Excludes limit (transport only), count (demand, not a constraint) and lock_scope (a partition, not a constraint). Quantities are reduced to base-unit magnitudes, so equivalent spellings of one query hash alike.

Return type:

str

class PassiveQuery(**kwargs)[source]#

Bases: PartQuery

Shared by the two-terminal passive categories.

tolerance: Final[Filter | None] = None#

Relative tolerance, dimensionless. Write 0.01 or 1 * pct.

class ResistorQuery(**kwargs)[source]#

Bases: PassiveQuery

Query for a resistor.

category: ClassVar[str] = 'resistor'#
value_key: ClassVar[str | None] = 'resistance'#
resistance: Final[Filter | None] = None#

10 * kohm.

Type:

Resistance, as a quantity

class CapacitorQuery(**kwargs)[source]#

Bases: PassiveQuery

Query for a capacitor.

category: ClassVar[str] = 'capacitor'#
value_key: ClassVar[str | None] = 'capacitance'#
capacitance: Final[Filter | None] = None#

100 * nF.

Type:

Capacitance, as a quantity

rated_voltage_dc: Final[Filter | None] = None#

AtLeast(16 * V).

Type:

Rated DC voltage, as a quantity

temperature_coefficient_code: Final[Filter | None] = None#

Dielectric code, such as “X7R” or “C0G”.

polarized: Final[Filter | None] = None#

Whether the part is polarized.

PolarizedCapacitor sets this; Capacitor leaves it unset and refuses a polarized part when it binds, so an unpolarized part needs no filter here.

class InductorQuery(**kwargs)[source]#

Bases: PassiveQuery

Query for an inductor.

category: ClassVar[str] = 'inductor'#
value_key: ClassVar[str | None] = 'inductance'#
inductance: Final[Filter | None] = None#

4.7 * uH.

Type:

Inductance, as a quantity

require_dimension(value, unit, *, provider, mpn, attribute)[source]#

Check a provider’s value carries the right dimensionality, and return it.

The whole of the unit contract: there is no magnitude heuristic and no conversion. Reported against the provider, with the part and attribute named.

Return type:

PlainQuantity

Parameters:
  • value (object)

  • unit (PlainQuantity)

  • provider (str)

  • mpn (str)

  • attribute (str)

quantity(text)[source]#

Parse a quantity through JITX’s shared unit registry.

Every quantity in this package has to come from jitx.units.registry: quantities from two registries do not compare.

Return type:

PlainQuantity

Parameters:

text (str)