"""
Query API for the JITX parts database.
This module provides a Python implementation of the query builder API
for the JITX parts database, translating the Stanza query-api.stanza to Python.
"""
from __future__ import annotations
import os
from dataclasses import dataclass, fields
import logging
from enum import Enum
from collections.abc import Sequence
from typing import (
Self,
TypedDict,
Unpack,
cast,
)
import jitx
import jitx.container
import jitx.inspect
from jitx.interval import Interval
import jitx.context
import jitx.units
import jitx._structural
from jitx import current
from .commands import PartJSON, dbquery, QueryParamValue
from ._types import ClassName
from ._types.component import (
Part as PartType,
Category,
ComponentCode,
ResistorEModel,
InductorEModel,
CapacitorEModel,
)
from ._types.resistor import Resistor as ResistorType
from ._types.capacitor import Capacitor as CapacitorType
from ._types.inductor import Inductor as InductorType
from ._types.main import to_component, download_model3d_files
from ._build import (
DBLandpattern,
create_dbsymbol,
build_two_pin_mappings,
build_generic_mappings,
_set_pin_on_object,
)
from ._sanitize import (
sanitize_pin,
python_component_name,
python_landpattern_name,
)
from ._types.landpattern import PinByNameCode
from .convert import insert_model3d_dir
# Configure logging
logging = logging.getLogger("jitx_parts_database.query_api")
# Special marker class to denote using distinct on a field
[docs]
class FindDistinctType:
"""Marker class for FIND_DISTINCT functionality.
Use the FIND_DISTINCT constant instead of creating instances directly.
"""
def __repr__(self) -> str:
return "FIND_DISTINCT"
def __str__(self) -> str:
return "FIND_DISTINCT"
# Special marker instance to denote using distinct on a field
FIND_DISTINCT = FindDistinctType()
[docs]
class AuthorizedVendor(Enum):
"""Authorized vendors for electronic components."""
JLCPCB = "JLCPCB"
LCSC = "LCSC"
DigiKey = "DigiKey"
Future = "Future"
Mouser = "Mouser"
Arrow = "Arrow"
Avnet = "Avnet"
Newark = "Newark"
[docs]
class SortDir(Enum):
"""Sort direction options."""
INCREASING = "increasing"
DECREASING = "decreasing"
[docs]
@dataclass(frozen=True)
class SortKey:
"""Sort key for database queries."""
key: str
direction: SortDir
[docs]
@dataclass(frozen=True)
class DistinctKey:
"""Distinct key for database queries."""
key: str
[docs]
@dataclass(frozen=True)
class ExistKeys:
"""Existence keys for database queries."""
keys: Sequence[str]
[docs]
class FindOptimum(Enum):
"""Find optimum options."""
FIND_MAXIMUM = "find_maximum"
FIND_MINIMUM = "find_minimum"
[docs]
class PartQueryDict(TypedDict, total=False):
trust: str | Sequence[str] | FindDistinctType | None
category: str | Sequence[str] | FindDistinctType | None
mpn: str | Sequence[str] | FindDistinctType | None
mounting: str | Sequence[str] | FindDistinctType | None
manufacturer: str | Sequence[str] | FindDistinctType | None
description: str | Sequence[str] | FindDistinctType | None
case: str | Sequence[str] | FindDistinctType | None
min_stock: int | FindDistinctType | None
quantity_needed: int | FindDistinctType | None
price: float | Interval | FindOptimum | Sequence[float] | FindDistinctType | None
x: float | Interval | Sequence[float] | FindDistinctType | None
y: float | Interval | Sequence[float] | FindDistinctType | None
z: float | Interval | Sequence[float] | FindDistinctType | None
area: float | Interval | FindOptimum | Sequence[float] | FindDistinctType | None
rated_temperature_min: float | Interval | Sequence[float] | FindDistinctType | None
rated_temperature_max: float | Interval | Sequence[float] | FindDistinctType | None
operating_temperature: Interval | FindDistinctType | None
stock: int | FindDistinctType | None
sellers: Sequence[str | AuthorizedVendor] | FindDistinctType | None
sort: SortKey | Sequence[SortKey] | FindDistinctType | None
exist: ExistKeys | FindDistinctType | None
distinct: DistinctKey | FindDistinctType | None
ignore_stock: bool | FindDistinctType | None
[docs]
class PassiveQueryDict(PartQueryDict, total=False):
type: str | Sequence[str] | FindDistinctType | None
tolerance: float | jitx.units.PlainQuantity | FindDistinctType | None
precision: float | jitx.units.PlainQuantity | FindDistinctType | None
tolerance_min: float | Interval | FindDistinctType | None
tolerance_max: float | Interval | FindDistinctType | None
component_datasheet: str | Sequence[str] | FindDistinctType | None
metadata_image: str | Sequence[str] | FindDistinctType | None
metadata_digi_key_part_number: str | Sequence[str] | FindDistinctType | None
metadata_description: str | Sequence[str] | FindDistinctType | None
metadata_packaging: str | Sequence[str] | FindDistinctType | None
[docs]
class ResistorQueryDict(PassiveQueryDict, total=False):
resistance: (
float
| jitx.units.PlainQuantity
| Interval
| Sequence[float]
| FindDistinctType
| None
)
rated_power: float | Interval | Sequence[float] | FindDistinctType | None
composition: str | FindDistinctType | None
tcr_pos: float | Interval | FindDistinctType | None
tcr_neg: float | Interval | FindDistinctType | None
metadata_series: str | FindDistinctType | None
metadata_features: str | FindDistinctType | None
metadata_supplier_device_package: str | FindDistinctType | None
metadata_number_of_terminations: int | FindDistinctType | None
[docs]
class CapacitorQueryDict(PassiveQueryDict, total=False):
capacitance: float | jitx.units.PlainQuantity | Interval | FindDistinctType | None
anode: str | FindDistinctType | None
electrolyte: str | FindDistinctType | None
esr: float | Interval | FindDistinctType | None
esr_frequency: float | Interval | FindDistinctType | None
rated_voltage: float | Interval | FindDistinctType | None
rated_voltage_ac: float | Interval | FindDistinctType | None
rated_current_pk: float | Interval | FindDistinctType | None
rated_current_rms: float | Interval | FindDistinctType | None
temperature_coefficient_code: str | FindDistinctType | None
temperature_coefficient_raw_data: str | FindDistinctType | None
temperature_coefficient_tolerance: float | FindDistinctType | None
temperature_coefficient_lower_temperature: float | FindDistinctType | None
temperature_coefficient_upper_temperature: float | FindDistinctType | None
temperature_coefficient_change: float | FindDistinctType | None
metadata_lifetime_temp: float | Interval | FindDistinctType | None
metadata_applications: str | FindDistinctType | None
metadata_ripple_current_low_frequency: float | Interval | FindDistinctType | None
metadata_ripple_current_high_frequency: float | Interval | FindDistinctType | None
metadata_lead_spacing: float | Interval | FindDistinctType | None
[docs]
class InductorQueryDict(PassiveQueryDict, total=False):
inductance: float | jitx.units.PlainQuantity | Interval | FindDistinctType | None
material_core: str | FindDistinctType | None
shielding: str | FindDistinctType | None
current_rating: float | Interval | FindDistinctType | None
saturation_current: float | Interval | FindDistinctType | None
dc_resistance: float | Interval | FindDistinctType | None
quality_factor: float | Interval | FindDistinctType | None
quality_factor_frequency: float | Interval | FindDistinctType | None
self_resonant_frequency: float | Interval | FindDistinctType | None
[docs]
@dataclass(frozen=True, kw_only=True)
class PartQuery(jitx.context.Context):
"""Context for part queries."""
trust: str | Sequence[str] | FindDistinctType | None = None
category: str | Sequence[str] | FindDistinctType | None = None
mpn: str | Sequence[str] | FindDistinctType | None = None
mounting: str | Sequence[str] | FindDistinctType | None = None
manufacturer: str | Sequence[str] | FindDistinctType | None = None
description: str | Sequence[str] | FindDistinctType | None = None
case: str | Sequence[str] | FindDistinctType | None = None
min_stock: int | FindDistinctType | None = None
quantity_needed: int | FindDistinctType | None = None
price: (
float | Interval | FindOptimum | Sequence[float] | FindDistinctType | None
) = None
x: float | Interval | Sequence[float] | FindDistinctType | None = None
y: float | Interval | Sequence[float] | FindDistinctType | None = None
z: float | Interval | Sequence[float] | FindDistinctType | None = None
area: float | Interval | FindOptimum | Sequence[float] | FindDistinctType | None = (
None
)
rated_temperature_min: (
float | Interval | Sequence[float] | FindDistinctType | None
) = None
rated_temperature_max: (
float | Interval | Sequence[float] | FindDistinctType | None
) = None
# more-intuitive shortcut for specifying the above two
operating_temperature: Interval | FindDistinctType | None = None
stock: int | FindDistinctType | None = None
sellers: Sequence[str | AuthorizedVendor] | FindDistinctType | None = None
sort: SortKey | Sequence[SortKey] | FindDistinctType | None = None
exist: ExistKeys | FindDistinctType | None = None
distinct: DistinctKey | FindDistinctType | None = None
ignore_stock: bool | FindDistinctType | None = None
[docs]
@classmethod
def from_query(cls, query: PartQuery) -> PartQuery:
"""Create a PartQuery from a PartQuery."""
# Filter params to only include fields defined in this class
valid_fields = {field.name for field in fields(cls)}
all_params = query.params()
filtered_params = {k: v for k, v in all_params.items() if k in valid_fields}
# Log warning for dropped parameters
dropped_params = set(all_params.keys()) - valid_fields
if dropped_params:
logging.warning(
f"Dropped parameters in PartQuery.from_query: {sorted(dropped_params)}"
)
return cls(**cast(PartQueryDict, filtered_params))
[docs]
def update(self, **kwargs: Unpack[PartQueryDict]) -> PartQuery:
"""Update the context with new values.
Args:
**kwargs: Keyword arguments corresponding to dataclass fields
Returns:
New PartQuery with updated values
"""
# Validate that all kwargs are valid field names
valid_fields = {field.name for field in fields(self)}
invalid_fields = set(kwargs.keys()) - valid_fields
if invalid_fields:
raise ValueError(f"Invalid fields for PartQuery: {invalid_fields}")
# type(self) is the constructor for the class (`cls` in classmethods). It may be a subclass of PartQuery.
return type(self)(**cast(PartQueryDict, {**self.params(), **kwargs}))
[docs]
def params(self) -> PartQueryDict:
"""Get the parameters for the query."""
return cast(
PartQueryDict, {k: v for k, v in self.__dict__.items() if v is not None}
)
# Context are immutable ("frozen") because when circuits are instantiated, there is memoization,
# the memoization key has all Contexts accessed by the first run of the circuit constructor.
[docs]
@dataclass(frozen=True, kw_only=True)
class PassiveQuery(PartQuery):
"""Context for passive queries."""
type: str | Sequence[str] | FindDistinctType | None = None
tolerance: float | jitx.units.PlainQuantity | FindDistinctType | None = None
precision: float | jitx.units.PlainQuantity | FindDistinctType | None = (
None # stanza version was typed as Percentage (FIXME: review)
)
tolerance_min: float | Interval | FindDistinctType | None = None
tolerance_max: float | Interval | FindDistinctType | None = None
component_datasheet: str | Sequence[str] | FindDistinctType | None = (
None # stanza version was typed as ? (FIXME: review)
)
metadata_image: str | Sequence[str] | FindDistinctType | None = (
None # stanza version was typed as ? (FIXME: review)
)
metadata_digi_key_part_number: str | Sequence[str] | FindDistinctType | None = None
metadata_description: str | Sequence[str] | FindDistinctType | None = None
metadata_packaging: str | Sequence[str] | FindDistinctType | None = (
None # stanza version was typed as ? (FIXME: review)
)
[docs]
@classmethod
def from_query(cls, query: PartQuery) -> PassiveQuery:
"""Create a PassiveQuery from a PartQuery."""
# Filter params to only include fields defined in this class
valid_fields = {field.name for field in fields(cls)}
all_params = query.params()
filtered_params = {k: v for k, v in all_params.items() if k in valid_fields}
# Log warning for dropped parameters
dropped_params = set(all_params.keys()) - valid_fields
if dropped_params:
logging.warning(
f"Dropped parameters in PassiveQuery.from_query: {sorted(dropped_params)}"
)
return cls(**cast(PassiveQueryDict, filtered_params))
[docs]
def update(self, **kwargs: Unpack[PassiveQueryDict]) -> PassiveQuery:
"""Update the context with new values.
Args:
**kwargs: Keyword arguments corresponding to dataclass fields
Returns:
New PassiveQuery with updated values
"""
# Validate that all kwargs are valid field names
valid_fields = {field.name for field in fields(self)}
invalid_fields = set(kwargs.keys()) - valid_fields
if invalid_fields:
raise ValueError(f"Invalid fields for PassiveQuery: {invalid_fields}")
return type(self)(**cast(PassiveQueryDict, {**self.params(), **kwargs}))
[docs]
def params(self) -> PassiveQueryDict:
"""Get the parameters for the query."""
return cast(
PassiveQueryDict, {k: v for k, v in self.__dict__.items() if v is not None}
)
[docs]
@dataclass(frozen=True, kw_only=True)
class ResistorQuery(PassiveQuery):
"""Context for resistor queries."""
category: str | Sequence[str] | FindDistinctType | None = "resistor"
resistance: (
float
| jitx.units.PlainQuantity
| Interval
| Sequence[float]
| FindDistinctType
| None
) = None
rated_power: float | Interval | Sequence[float] | FindDistinctType | None = None
composition: str | FindDistinctType | None = None
tcr_pos: float | Interval | FindDistinctType | None = None
tcr_neg: float | Interval | FindDistinctType | None = None
metadata_series: str | FindDistinctType | None = None
metadata_features: str | FindDistinctType | None = (
None # stanza version was typed as ? (FIXME: review)
)
metadata_supplier_device_package: str | FindDistinctType | None = None
metadata_number_of_terminations: int | FindDistinctType | None = None
def __post_init__(self):
"""Validate that category is set to 'resistor'."""
if not (isinstance(self.category, str) and self.category == "resistor"):
raise ValueError(
f'ResistorQuery category must be "resistor", got {self.category!r}'
)
[docs]
@classmethod
def from_query(cls, query: PartQuery) -> ResistorQuery:
"""Create a ResistorQuery from a PartQuery."""
# Filter params to only include fields defined in this class
valid_fields = {field.name for field in fields(cls)}
all_params = query.params()
filtered_params = {
k: v
for k, v in all_params.items()
if k in valid_fields and not (k == "category" and v != "resistor")
}
# Log warning for dropped parameters
dropped_params = set(all_params.keys()) - set(filtered_params.keys())
if dropped_params:
logging.warning(
f"Dropped parameters in ResistorQuery.from_query: {sorted(dropped_params)}"
)
return cls(**cast(ResistorQueryDict, filtered_params))
[docs]
def update(self, **kwargs: Unpack[ResistorQueryDict]) -> ResistorQuery:
"""Update the context with new values.
Args:
**kwargs: Keyword arguments corresponding to dataclass fields
Returns:
New ResistorQuery with updated values
"""
# Validate that all kwargs are valid field names
valid_fields = {field.name for field in fields(self)}
invalid_fields = set(kwargs.keys()) - valid_fields
if invalid_fields:
raise ValueError(f"Invalid fields for ResistorQuery: {invalid_fields}")
return type(self)(**cast(ResistorQueryDict, {**self.params(), **kwargs}))
[docs]
@classmethod
def refine(cls, **kwargs: Unpack[ResistorQueryDict]) -> ResistorQuery:
"""Refine an existing context with additional values."""
return cls.require().update(**kwargs)
[docs]
def params(self) -> ResistorQueryDict:
"""Get the parameters for the query."""
return cast(
ResistorQueryDict, {k: v for k, v in self.__dict__.items() if v is not None}
)
[docs]
@dataclass(frozen=True, kw_only=True)
class CapacitorQuery(PassiveQuery):
"""Context for capacitor queries."""
category: str | Sequence[str] | FindDistinctType | None = "capacitor"
capacitance: (
float | jitx.units.PlainQuantity | Interval | FindDistinctType | None
) = None
anode: str | FindDistinctType | None = None
electrolyte: str | FindDistinctType | None = None
esr: float | Interval | FindDistinctType | None = None
esr_frequency: float | Interval | FindDistinctType | None = None
rated_voltage: float | Interval | FindDistinctType | None = None
rated_voltage_ac: float | Interval | FindDistinctType | None = None
rated_current_pk: float | Interval | FindDistinctType | None = None
rated_current_rms: float | Interval | FindDistinctType | None = None
temperature_coefficient_code: str | FindDistinctType | None = (
None # stanza version was 'temperature-coefficient_code' and typed as ? (FIXME: review)
)
temperature_coefficient_raw_data: str | FindDistinctType | None = (
None # stanza version was 'temperature-coefficient_raw-data' and typed as ? (FIXME: review). This is user-facing for 'temperature-coefficient.raw_data'.
)
temperature_coefficient_tolerance: float | FindDistinctType | None = (
None # stanza version was 'temperature-coefficient_tolerance' and typed as ? (FIXME: review)
)
temperature_coefficient_lower_temperature: float | FindDistinctType | None = (
None # stanza version was 'temperature-coefficient_lower-temperature' and typed as ? (FIXME: review)
)
temperature_coefficient_upper_temperature: float | FindDistinctType | None = (
None # stanza version was 'temperature-coefficient_upper-temperature' and typed as ? (FIXME: review)
)
temperature_coefficient_change: float | FindDistinctType | None = (
None # stanza version was 'temperature-coefficient_change' and typed as ? (FIXME: review)
)
metadata_lifetime_temp: float | Interval | FindDistinctType | None = None
metadata_applications: str | FindDistinctType | None = (
None # stanza version was typed as ? (FIXME: review)
)
metadata_ripple_current_low_frequency: (
float | Interval | FindDistinctType | None
) = None
metadata_ripple_current_high_frequency: (
float | Interval | FindDistinctType | None
) = None
metadata_lead_spacing: float | Interval | FindDistinctType | None = None
def __post_init__(self):
"""Validate that category is set to 'capacitor'."""
if not (isinstance(self.category, str) and self.category == "capacitor"):
raise ValueError(
f'CapacitorQuery category must be "capacitor", got {self.category!r}'
)
[docs]
@classmethod
def from_query(cls, query: PartQuery) -> CapacitorQuery:
"""Create a CapacitorQuery from a PartQuery."""
# Filter params to only include fields defined in this class
valid_fields = {field.name for field in fields(cls)}
all_params = query.params()
filtered_params = {
k: v
for k, v in all_params.items()
if k in valid_fields and not (k == "category" and v != "capacitor")
}
# Log warning for dropped parameters
dropped_params = set(all_params.keys()) - set(filtered_params.keys())
if dropped_params:
logging.warning(
f"Dropped parameters in CapacitorQuery.from_query: {sorted(dropped_params)}"
)
return cls(**cast(CapacitorQueryDict, filtered_params))
[docs]
def update(self, **kwargs: Unpack[CapacitorQueryDict]) -> CapacitorQuery:
"""Update the context with new values.
Args:
**kwargs: Keyword arguments corresponding to dataclass fields
Returns:
New CapacitorQuery with updated values
"""
# Validate that all kwargs are valid field names
valid_fields = {field.name for field in fields(self)}
invalid_fields = set(kwargs.keys()) - valid_fields
if invalid_fields:
raise ValueError(f"Invalid fields for CapacitorQuery: {invalid_fields}")
return type(self)(**cast(CapacitorQueryDict, {**self.params(), **kwargs}))
[docs]
@classmethod
def refine(cls, **kwargs: Unpack[CapacitorQueryDict]) -> CapacitorQuery:
"""Refine an existing context with additional values."""
return cls.require().update(**kwargs)
[docs]
def params(self) -> CapacitorQueryDict:
"""Get the parameters for the query."""
return cast(
CapacitorQueryDict,
{k: v for k, v in self.__dict__.items() if v is not None},
)
[docs]
@dataclass(frozen=True, kw_only=True)
class InductorQuery(PassiveQuery):
"""Context for inductor queries."""
category: str | Sequence[str] | FindDistinctType | None = "inductor"
inductance: (
float | jitx.units.PlainQuantity | Interval | FindDistinctType | None
) = None
material_core: str | FindDistinctType | None = None
shielding: str | FindDistinctType | None = None
current_rating: float | Interval | FindDistinctType | None = None
saturation_current: float | Interval | FindDistinctType | None = None
dc_resistance: float | Interval | FindDistinctType | None = None
quality_factor: float | Interval | FindDistinctType | None = None
quality_factor_frequency: float | Interval | FindDistinctType | None = None
self_resonant_frequency: float | Interval | FindDistinctType | None = None
def __post_init__(self):
"""Validate that category is set to 'inductor'."""
if not (isinstance(self.category, str) and self.category == "inductor"):
raise ValueError(
f'InductorQuery category must be "inductor", got {self.category!r}'
)
[docs]
@classmethod
def from_query(cls, query: PartQuery) -> InductorQuery:
"""Create an InductorQuery from a PartQuery."""
# Filter params to only include fields defined in this class
valid_fields = {field.name for field in fields(cls)}
all_params = query.params()
filtered_params = {
k: v
for k, v in all_params.items()
if k in valid_fields and not (k == "category" and v != "inductor")
}
# Log warning for dropped parameters
dropped_params = set(all_params.keys()) - set(filtered_params.keys())
if dropped_params:
logging.warning(
f"Dropped parameters in InductorQuery.from_query: {sorted(dropped_params)}"
)
return cls(**cast(InductorQueryDict, filtered_params))
[docs]
def update(self, **kwargs: Unpack[InductorQueryDict]) -> InductorQuery:
"""Update the context with new values.
Args:
**kwargs: Keyword arguments corresponding to dataclass fields
Returns:
New InductorQuery with updated values
"""
# Validate that all kwargs are valid field names
valid_fields = {field.name for field in fields(self)}
invalid_fields = set(kwargs.keys()) - valid_fields
if invalid_fields:
raise ValueError(f"Invalid fields for InductorQuery: {invalid_fields}")
return type(self)(**cast(InductorQueryDict, {**self.params(), **kwargs}))
[docs]
@classmethod
def refine(cls, **kwargs: Unpack[InductorQueryDict]) -> InductorQuery:
"""Refine an existing context with additional values."""
return cls.require().update(**kwargs)
[docs]
def params(self) -> InductorQueryDict:
"""Get the parameters for the query."""
return cast(
InductorQueryDict, {k: v for k, v in self.__dict__.items() if v is not None}
)
[docs]
class TwoPinShortTrace(Enum):
"""Short trace options for two-pin components."""
SHORT_TRACE_BOTH = "short_trace_both"
SHORT_TRACE_ANODE = "short_trace_anode"
SHORT_TRACE_CATHODE = "short_trace_cathode"
SHORT_TRACE_NEITHER = "short_trace_neither"
# Key name overrides
_key_overrides = {
"esr-frequency": "esr_frequency",
"temperature-coefficient_raw-data": "temperature-coefficient.raw_data",
"quantity-needed": "max-minimum_quantity",
"stock!": "_stock",
# Those were handled as overrides in stanza but are now handled in extract.
# "sellers!": "_sellers",
# "sort!": "_sort",
# "exist!": "_exist",
# "distinct!": "_distinct",
}
[docs]
def to_db_key(key: str) -> str:
"""Convert a Python key to the correct database key, applying underscore-to-dot mapping and overrides."""
if key in _key_overrides:
return _key_overrides[key]
return key.replace("_", ".")
# Map python keyword argument names to stanza keywords
_key_mapping = {
"trust": "trust",
"category": "category",
"mpn": "mpn",
"mounting": "mounting",
"manufacturer": "manufacturer",
"description": "description",
"case": "case",
"min_stock": "min-stock",
"quantity_needed": "quantity-needed",
"price": "price",
"x": "x",
"y": "y",
"z": "z",
"area": "area",
"rated_temperature_min": "rated-temperature_min",
"rated_temperature_max": "rated-temperature_max",
"operating_temperature": "operating-temperature",
"stock": "stock!",
"sellers": "sellers!",
"sort": "sort!",
"exist": "exist!",
"distinct": "distinct!",
"ignore_stock": "ignore-stock",
# Passive keys
"type": "type",
"tolerance": "tolerance",
"precision": "precision",
"tolerance_min": "tolerance_min",
"tolerance_max": "tolerance_max",
"component_datasheet": "component_datasheet",
"metadata_image": "metadata_image",
"metadata_digi_key_part_number": "metadata_digi-key-part-number",
"metadata_description": "metadata_description",
"metadata_packaging": "metadata_packaging",
# Resistor keys
"resistance": "resistance",
"rated_power": "rated-power",
"composition": "composition",
"tcr_pos": "tcr_pos",
"tcr_neg": "tcr_neg",
"metadata_series": "metadata_series",
"metadata_features": "metadata_features",
"metadata_supplier_device_package": "metadata_supplier-device-package",
"metadata_number_of_terminations": "metadata_number-of-terminations",
# Capacitor keys
"capacitance": "capacitance",
"anode": "anode",
"electrolyte": "electrolyte",
"esr": "esr",
"esr_frequency": "esr-frequency",
"rated_voltage": "rated-voltage",
"rated_voltage_ac": "rated-voltage-ac",
"rated_current_pk": "rated-current-pk",
"rated_current_rms": "rated-current-rms",
"temperature_coefficient_code": "temperature-coefficient_code",
"temperature_coefficient_raw_data": "temperature-coefficient_raw-data",
"temperature_coefficient_tolerance": "temperature-coefficient_tolerance",
"temperature_coefficient_lower_temperature": "temperature-coefficient_lower-temperature",
"temperature_coefficient_upper_temperature": "temperature-coefficient_upper-temperature",
"temperature_coefficient_change": "temperature-coefficient_change",
"metadata_lifetime_temp": "metadata_lifetime-temp",
"metadata_applications": "metadata_applications",
"metadata_ripple_current_low_frequency": "metadata_ripple-current-low-frequency",
"metadata_ripple_current_high_frequency": "metadata_ripple-current-high-frequency",
"metadata_lead_spacing": "metadata_lead-spacing",
# Inductor keys
"inductance": "inductance",
"material_core": "material-core",
"shielding": "shielding",
"current_rating": "current-rating",
"saturation_current": "saturation-current",
"dc_resistance": "dc-resistance",
"quality_factor": "quality-factor",
"quality_factor_frequency": "quality-factor-frequency",
"self_resonant_frequency": "self-resonant-frequency",
}
ProcessedQueryParamValue = (
DistinctKey
| ExistKeys
| Interval
| SortKey
| Sequence[SortKey]
| bool
| float
| int
| str
)
[docs]
def preprocess_keyword_args(
kwargs: PartQueryDict,
) -> dict[str, ProcessedQueryParamValue]:
"""Preprocess keyword arguments for queries.
Args:
kwargs: Keyword arguments to preprocess
Returns:
Preprocessed arguments
"""
result = {}
# There can only be one FIND_DISTINCT parameter, otherwise raise an error.
distinct_params = [
key for key, value in kwargs.items() if isinstance(value, FindDistinctType)
]
if len(distinct_params) > 1:
raise ValueError(
"There can only be one FIND_DISTINCT parameter. Got: {distinct_params}"
)
# There can only be one FIND_OPTIMUM parameter, otherwise raise an error.
find_optimum_params = [
key for key, value in kwargs.items() if isinstance(value, FindOptimum)
]
if len(find_optimum_params) > 1:
raise ValueError(
"There can only be one FIND_OPTIMUM parameter. Got: {find_optimum_params}"
)
# There cannot be both tolerance and precision, otherwise raise an error.
if kwargs.get("tolerance") is not None and kwargs.get("precision") is not None:
raise ValueError(
"Cannot specify both 'tolerance' and 'precision' in Parts DB parameters."
)
# Special handling for FindOptimum and FindDistinct
for original_key, value in kwargs.items():
if original_key not in _key_mapping:
raise ValueError(f"Unknown Parts DB query parameter: {original_key}")
# Special handling for FIND_DISTINCT
if isinstance(value, FindDistinctType):
result["distinct!"] = DistinctKey(original_key)
# Special handling for FindOptimum
elif isinstance(value, FindOptimum):
direction = (
SortDir.DECREASING
if value == FindOptimum.FIND_MAXIMUM
else SortDir.INCREASING
)
result["sort!"] = SortKey(original_key, direction)
# Special handling for precision (converts to tolerance)
elif original_key == "precision":
result["tolerance"] = value
# Normal case
else:
key = _key_mapping[original_key]
result[key] = value
return result
SMD_PKGS = (
"009005",
"0301m",
"01005",
"0402m",
"0201",
"0603m",
"0202",
"0606m",
"0204",
"0510m",
"Wide 0402",
"0306",
"0816m",
"Wide 0603",
"0402",
"1005m",
"0505",
"1414m",
"0508",
"1220m",
"Wide 0805",
"0603",
"1608m",
"0612",
"1632m",
"Wide 1206",
"0805",
"2012m",
"1111",
"2828m",
"1206",
"3216m",
"1210",
"3225m",
"1218",
"3246m",
"Wide 1812",
"1225",
"3263m",
"Wide 2512",
"1530",
"3876m",
"Wide 3015",
"1808",
"4520m",
"1812",
"4532m",
"1825",
"4564m",
"1835",
"4589",
"Wide 3518",
"5020m",
"2010",
"5025m",
"2043",
"Wide 4320",
"2220",
"5750m",
"2225",
"5763m",
"2312",
"6032m",
"2512",
"6331m",
"2725",
"7142m",
"2728",
"7142m",
"Wide 2827",
"2816",
"2817",
"7142m",
"2953",
"Wide 5929",
"3920",
"1052m",
)
[docs]
def is_valid_smd_pkg(pkg: str) -> bool:
"""Check if a package is a valid SMD package."""
return pkg in SMD_PKGS
# Helper functions for valid packages
[docs]
def valid_smd_pkgs(min_pkg: str = "0402") -> Sequence[str]:
"""Get a list of valid SMD packages.
Args:
min_pkg: Minimum package size
Returns:
List of valid SMD packages
"""
if not is_valid_smd_pkg(min_pkg):
raise ValueError(
f"Unknown SMD package requested as minimum: {min_pkg}. The known packages are:\n{SMD_PKGS}"
)
start_idx = SMD_PKGS.index(min_pkg)
return SMD_PKGS[start_idx:]
# Top-level query builder functions
[docs]
def make_resistor_query(
qb: PartQuery | None = None, **kwargs: Unpack[ResistorQueryDict]
) -> ResistorQuery:
"""Make a ResistorQuery from a PartQuery or ResistorQuery.
Args:
qb: Optional base query context
**kwargs: Additional query parameters
Returns:
ResistorQuery with the specified parameters
"""
base_query = qb or ResistorQuery.get() or PartQuery.get()
if isinstance(base_query, ResistorQuery):
return base_query.update(**kwargs)
elif isinstance(base_query, PartQuery):
return ResistorQuery.from_query(base_query).update(**kwargs)
else:
return ResistorQuery(**kwargs)
[docs]
def make_capacitor_query(
qb: PartQuery | None = None, **kwargs: Unpack[CapacitorQueryDict]
) -> CapacitorQuery:
"""Make a CapacitorQuery from a PartQuery or CapacitorQuery.
Args:
qb: Optional base query context
**kwargs: Additional query parameters
Returns:
CapacitorQuery with the specified parameters
"""
base_query = qb or CapacitorQuery.get() or PartQuery.get()
if isinstance(base_query, CapacitorQuery):
return base_query.update(**kwargs)
elif isinstance(base_query, PartQuery):
return CapacitorQuery.from_query(base_query).update(**kwargs)
else:
return CapacitorQuery(**kwargs)
[docs]
def make_inductor_query(
qb: PartQuery | None = None, **kwargs: Unpack[InductorQueryDict]
) -> InductorQuery:
"""Make an InductorQuery from a PartQuery or InductorQuery.
Args:
qb: Optional base query context
**kwargs: Additional query parameters
Returns:
InductorQuery with the specified parameters
"""
base_query = qb or InductorQuery.get() or PartQuery.get()
if isinstance(base_query, InductorQuery):
return base_query.update(**kwargs)
elif isinstance(base_query, PartQuery):
return InductorQuery.from_query(base_query).update(**kwargs)
else:
return InductorQuery(**kwargs)
[docs]
def make_part_query(
qb: PartQuery | None = None, **kwargs: Unpack[PartQueryDict]
) -> PartQuery:
"""Make a PartQuery from a PartQuery.
Args:
qb: Optional base query context
**kwargs: Additional query parameters
Returns:
PartQuery with the specified parameters
"""
base_query = qb or PartQuery.get()
if isinstance(base_query, PartQuery):
return base_query.update(**kwargs)
else:
return PartQuery(**kwargs)
# Internal functions for creating components
def _internal_create_components(qb: PartQuery, limit: int) -> Sequence[PartType]:
"""Internal function to create components from a query.
Args:
qb: Query context
limit: Maximum number of components to create
Returns:
List of components
Raises:
ValueError: If no components meet the requirements
"""
params = extract(qb)
results: Sequence[PartJSON] = dbquery(params, limit)
if not results:
raise ValueError(f"No components meeting requirements: {params}")
return [to_component(result) for result in results]
def _internal_create_component(qb: PartQuery) -> PartType:
"""Internal function to create a single component from a query.
Args:
qb: Query context
Returns:
PartType
Raises:
ValueError: If no components meet the requirements
"""
comps = _internal_create_components(qb, 1)
if not comps:
raise ValueError("Component list is empty")
return comps[0]
# Public API for creating components
[docs]
class InsertContainer(jitx.container.Container):
nets: list[jitx.Net]
a_short_trace: jitx.net.ShortTrace | None
c_short_trace: jitx.net.ShortTrace | None
def _set_metadata(comp: jitx.Component, cc: ComponentCode) -> None:
"""Set common metadata fields on a component from ComponentCode."""
comp.mpn = cc.mpn
comp.manufacturer = cc.manufacturer
comp.reference_designator_prefix = cc.reference_prefix
comp.datasheet = cc.datasheet
def _build_landpattern(cc: ComponentCode) -> tuple[ComponentCode, DBLandpattern]:
"""Build landpattern with model3d handling from ComponentCode.
Returns the (possibly mutated) ComponentCode and the DBLandpattern.
Must be absolute — Model3D resolves relative paths from its own source file, not cwd.
Creates a dynamic subclass so the instance's class __name__ matches the landpattern name.
"""
model3d_dir = os.path.join(os.getcwd(), "3d-models")
cc = insert_model3d_dir(cc, model3d_dir)
failed_3d = download_model3d_files(cc)
assert cc.landpattern is not None
lp_class_name = python_landpattern_name(cc.landpattern.name)
lp = DBLandpattern(cc, missing_3d_models=failed_3d)
ClassName(lp_class_name).assign(lp)
return cc, lp
# Note from Philippe : yeah... this is neat and all but delete if it is not robust and it breaks.
def _set_component_class_name(comp: jitx.Component, cc: ComponentCode) -> None:
"""Set the component's class to a dynamic subclass so __name__ matches the part.
Uses passivate() to create the class (prevents __init_subclass__ from triggering
magic_context on Instantiable descriptors), then registers a postprocessor to
change __class__ after the prepost type(self)-is-cls check has already passed —
a direct assignment inside __init__ would break Component's @early/@late handlers.
"""
comp_class_name = python_component_name(cc.mpn, cc.name)
ClassName(comp_class_name).assign(comp)
def _validate_percent(value: object, name: str) -> None:
"""Assert that a quantity is a dimensionless percentage."""
if isinstance(value, jitx.units.PlainQuantity):
assert (value.dimensionality, value.units) == (
jitx.units.percent.dimensionality,
jitx.units.percent,
), (
f"{name} Quantity requested by the user from the Parts DB "
f"only support a dimensionless percentage, got: '{value.units}'."
)
[docs]
class Resistor(jitx.Component):
"""Resistor component.
Args:
query: query to optionally override the current ResistorQuery or PartQuery from the DesignContext.
comp_name: name of the component to be created.
**kwargs: Additional query parameters
Raises:
Exception: If no components meet the requirements
"""
mpn: str
manufacturer: str
datasheet: str
reference_designator_prefix: str
value: jitx.units.PlainQuantity
p1: jitx.Port
p2: jitx.Port
landpattern: jitx.Landpattern
symbol: jitx.Symbol
cmappings: list[jitx.PadMapping | jitx.SymbolMapping]
data: ResistorType
def __init__(
self,
query: PartQuery | None = None,
*,
comp_name: str | None = None,
**kwargs: Unpack[ResistorQueryDict],
):
rq = make_resistor_query(query, **kwargs)
if isinstance(rq.resistance, jitx.units.PlainQuantity):
assert rq.resistance.dimensionality == jitx.units.ohm.dimensionality, (
f"Resistance Quantity requested by the user from the Parts DB is not in a compatible unit: '{rq.resistance.units}'."
)
_validate_percent(rq.tolerance, "Tolerance")
_validate_percent(rq.precision, "Precision")
try:
part = _internal_create_component(rq)
if not isinstance(part, ResistorType):
raise TypeError("Component returned is not a Resistor")
self.data = part
cc = part.component
_set_metadata(self, cc)
_set_component_class_name(self, cc)
if isinstance(rq.resistance, jitx.units.PlainQuantity):
self.value = rq.resistance
else:
emodel = cc.emodel.value if cc.emodel else None
assert (
isinstance(emodel, ResistorEModel) and emodel.resistance is not None
), "Missing resistance value from Parts DB"
self.value = jitx.units.PlainQuantity(
emodel.resistance, jitx.units.ohm
).to_compact()
from jitxlib.symbols.resistor import ResistorSymbol
self.symbol = ResistorSymbol()
cc, self.landpattern = _build_landpattern(cc)
self.p1 = jitx.Port()
self.p2 = jitx.Port()
p1, p2, *_ = list(cc.pin_properties.pins)
if len(_) != 0:
raise ValueError(
f"Expected exactly two ports for resistors from the Parts DB, got extras: {_}"
)
self.cmappings = build_two_pin_mappings(
self, self.landpattern, self.symbol, p1, p2
)
except Exception as e:
arg_list = "\n- ".join(f"{k}: {v}" for k, v in extract(rq).items())
logging.error(f"Failed to create resistor for query:\n- {arg_list}\n{e}")
raise
[docs]
def insert(
self,
pin_a: jitx.Port | jitx.Net,
pin_b: jitx.Port | jitx.Net,
*,
short_trace: TwoPinShortTrace | bool = TwoPinShortTrace.SHORT_TRACE_NEITHER,
) -> Self:
"""Insert this resistor between two pins of a circuit.
Args:
self: Component
pin_a: First pin
pin_b: Second pin
short_trace: Short trace option
"""
c = InsertContainer()
c.nets = [
pin_a + self.p1,
pin_b + self.p2,
]
st = to_short_trace_enum(short_trace)
if (
st == TwoPinShortTrace.SHORT_TRACE_BOTH
or st == TwoPinShortTrace.SHORT_TRACE_ANODE
):
if not isinstance(pin_a, jitx.Port):
raise ValueError(
"Cannot make a shortrace with a net. Give a port to Resistor.insert's pin_a."
)
c.a_short_trace = jitx.net.ShortTrace(pin_a, self.p1)
if (
st == TwoPinShortTrace.SHORT_TRACE_BOTH
or st == TwoPinShortTrace.SHORT_TRACE_CATHODE
):
if not isinstance(pin_b, jitx.Port):
raise ValueError(
"Cannot make a shortrace with a net. Give a port to Resistor.insert's pin_b."
)
c.c_short_trace = jitx.net.ShortTrace(pin_b, self.p2)
circuit = current.circuit
circuit += c
return self
[docs]
class Capacitor(jitx.Component):
"""Capacitor component.
Args:
query: query to optionally override the current CapacitorQuery or PartQuery from the DesignContext.
comp_name: name of the component to be created.
polarized: whether the capacitor is polarized.
**kwargs: Additional query parameters
Raises:
Exception: If no components meet the requirements
"""
mpn: str
manufacturer: str
datasheet: str
reference_designator_prefix: str
value: jitx.units.PlainQuantity
p1: jitx.Port
p2: jitx.Port
landpattern: jitx.Landpattern
symbol: jitx.Symbol
cmappings: list[jitx.PadMapping | jitx.SymbolMapping]
# True if p1 is the anode and p2 is the cathode (pin names a and c in parts db)
polarized: bool
data: CapacitorType
def __init__(
self,
query: PartQuery | None = None,
*,
comp_name: str | None = None,
polarized: bool = False,
**kwargs: Unpack[CapacitorQueryDict],
):
rq = make_capacitor_query(query, **kwargs)
if isinstance(rq.capacitance, jitx.units.PlainQuantity):
assert rq.capacitance.dimensionality == jitx.units.F.dimensionality, (
f"Capacitance Quantity requested by the user from the Parts DB is not in a compatible unit: '{rq.capacitance.units}'."
)
_validate_percent(rq.tolerance, "Tolerance")
_validate_percent(rq.precision, "Precision")
try:
part = _internal_create_component(rq)
if not isinstance(part, CapacitorType):
raise TypeError("Component returned is not a Capacitor")
self.data = part
cc = part.component
_set_metadata(self, cc)
_set_component_class_name(self, cc)
if isinstance(rq.capacitance, jitx.units.PlainQuantity):
self.value = rq.capacitance
else:
emodel = cc.emodel.value if cc.emodel else None
assert (
isinstance(emodel, CapacitorEModel)
and emodel.capacitance is not None
), "Missing capacitance value from Parts DB"
self.value = jitx.units.PlainQuantity(
emodel.capacitance, jitx.units.F
).to_compact()
# Detect polarization from pin names (a = anode, c = cathode)
pin_name_set = {
sanitize_pin(pin.pin).value.pin_name
for pin in cc.pin_properties.pins
if isinstance(sanitize_pin(pin.pin).value, PinByNameCode)
}
self.polarized = pin_name_set == {"a", "c"}
assert not (polarized and not self.polarized), (
f"The Parts DB returned the non-polarized capacitor {cc.mpn} to a PolarizedCapacitor instantiation. "
"PolarizedCapacitor is just a type extension to expose the anode pin as `a` and the cathode pin as `c`. "
"It is the responsibility of the user to ensure the returned component is polarized. "
"Change or restrict the query to match a polarized component."
)
if self.polarized:
from jitxlib.symbols.capacitor import PolarizedCapacitorSymbol
self.symbol = PolarizedCapacitorSymbol()
else:
from jitxlib.symbols.capacitor import CapacitorSymbol
self.symbol = CapacitorSymbol()
cc, self.landpattern = _build_landpattern(cc)
self.p1 = jitx.Port()
self.p2 = jitx.Port()
p1, p2, *_ = list(cc.pin_properties.pins)
if len(_) != 0:
raise ValueError(
f"Expected exactly two ports for capacitors from the Parts DB, got extras: {_}"
)
if self.polarized:
assert isinstance(p1.pin.value, PinByNameCode) and isinstance(
p2.pin.value, PinByNameCode
)
if (p1.pin.value.pin_name, p2.pin.value.pin_name) != ("a", "c"):
p1, p2 = p2, p1
self.cmappings = build_two_pin_mappings(
self, self.landpattern, self.symbol, p1, p2
)
except Exception as e:
arg_list = "\n- ".join(f"{k}: {v}" for k, v in extract(rq).items())
logging.error(f"Failed to create capacitor for query:\n- {arg_list}\n{e}")
raise
[docs]
def insert(
self,
pin_a: jitx.Port | jitx.Net,
pin_b: jitx.Port | jitx.Net,
*,
short_trace: TwoPinShortTrace | bool = TwoPinShortTrace.SHORT_TRACE_NEITHER,
) -> Self:
"""Insert this capacitor between two pins of a circuit.
Args:
self: Component
pin_a: First pin
pin_b: Second pin
short_trace: Short trace option
"""
c = InsertContainer()
c.nets = [
pin_a + self.p1,
pin_b + self.p2,
]
st = to_short_trace_enum(short_trace)
if (
st == TwoPinShortTrace.SHORT_TRACE_BOTH
or st == TwoPinShortTrace.SHORT_TRACE_ANODE
):
if not isinstance(pin_a, jitx.Port):
raise ValueError(
"Cannot make a shortrace with a net. Give a port to Capacitor.insert's pin_a."
)
c.a_short_trace = jitx.net.ShortTrace(pin_a, self.p1)
if (
st == TwoPinShortTrace.SHORT_TRACE_BOTH
or st == TwoPinShortTrace.SHORT_TRACE_CATHODE
):
if not isinstance(pin_b, jitx.Port):
raise ValueError(
"Cannot make a shortrace with a net. Give a port to Capacitor.insert's pin_b."
)
c.c_short_trace = jitx.net.ShortTrace(pin_b, self.p2)
circuit = current.circuit
circuit += c
return self
[docs]
class PolarizedCapacitor(Capacitor):
"""Polarized capacitor component."""
a: jitx.Port
c: jitx.Port
def __init__(self, *args, **kwargs):
super().__init__(*args, polarized=True, **kwargs)
assert self.polarized, (
"PolarizedCapacitor must be polarized, it is not a polarized capacitor in the parts DB."
)
# Port aliases
self.a = jitx._structural.Proxy.create(self.p1, ref=True)
self.c = jitx._structural.Proxy.create(self.p2, ref=True)
[docs]
class Inductor(jitx.Component):
"""Inductor component.
Args:
query: query to optionally override the current InductorQuery or PartQuery from the DesignContext.
comp_name: name of the component to be created.
**kwargs: Additional query parameters
Raises:
Exception: If no components meet the requirements
"""
mpn: str
manufacturer: str
datasheet: str
reference_designator_prefix: str
value: jitx.units.PlainQuantity
p1: jitx.Port
p2: jitx.Port
landpattern: jitx.Landpattern
symbol: jitx.Symbol
cmappings: list[jitx.PadMapping | jitx.SymbolMapping]
data: InductorType
def __init__(
self,
query: PartQuery | None = None,
*,
comp_name: str | None = None,
**kwargs: Unpack[InductorQueryDict],
):
rq = make_inductor_query(query, **kwargs)
if isinstance(rq.inductance, jitx.units.PlainQuantity):
assert rq.inductance.dimensionality == jitx.units.H.dimensionality, (
f"Inductance Quantity requested by the user from the Parts DB is not in a compatible unit: '{rq.inductance.units}'."
)
_validate_percent(rq.tolerance, "Tolerance")
_validate_percent(rq.precision, "Precision")
try:
part = _internal_create_component(rq)
if not isinstance(part, InductorType):
raise TypeError("Component returned is not an Inductor")
self.data = part
cc = part.component
_set_metadata(self, cc)
_set_component_class_name(self, cc)
if isinstance(rq.inductance, jitx.units.PlainQuantity):
self.value = rq.inductance
else:
emodel = cc.emodel.value if cc.emodel else None
assert (
isinstance(emodel, InductorEModel) and emodel.inductance is not None
), "Missing inductance value from Parts DB"
self.value = jitx.units.PlainQuantity(
emodel.inductance, jitx.units.H
).to_compact()
from jitxlib.symbols.inductor import InductorSymbol
self.symbol = InductorSymbol()
cc, self.landpattern = _build_landpattern(cc)
self.p1 = jitx.Port()
self.p2 = jitx.Port()
p1, p2, *_ = list(cc.pin_properties.pins)
if len(_) != 0:
raise ValueError(
f"Expected exactly two ports for inductors from the Parts DB, got extras: {_}"
)
self.cmappings = build_two_pin_mappings(
self, self.landpattern, self.symbol, p1, p2
)
except Exception as e:
arg_list = "\n- ".join(f"{k}: {v}" for k, v in extract(rq).items())
logging.error(f"Failed to create inductor for query:\n- {arg_list}\n{e}")
raise
[docs]
def insert(
self,
pin_a: jitx.Port | jitx.Net,
pin_b: jitx.Port | jitx.Net,
*,
short_trace: TwoPinShortTrace | bool = TwoPinShortTrace.SHORT_TRACE_NEITHER,
) -> Self:
"""Insert this inductor between two pins of a circuit.
Args:
self: Component
pin_a: First pin
pin_b: Second pin
short_trace: Short trace option
"""
c = InsertContainer()
c.nets = [
pin_a + self.p1,
pin_b + self.p2,
]
st = to_short_trace_enum(short_trace)
if (
st == TwoPinShortTrace.SHORT_TRACE_BOTH
or st == TwoPinShortTrace.SHORT_TRACE_ANODE
):
if not isinstance(pin_a, jitx.Port):
raise ValueError(
"Cannot make a shortrace with a net. Give a port to Inductor.insert's pin_a."
)
c.a_short_trace = jitx.net.ShortTrace(pin_a, self.p1)
if (
st == TwoPinShortTrace.SHORT_TRACE_BOTH
or st == TwoPinShortTrace.SHORT_TRACE_CATHODE
):
if not isinstance(pin_b, jitx.Port):
raise ValueError(
"Cannot make a shortrace with a net. Give a port to Inductor.insert's pin_b."
)
c.c_short_trace = jitx.net.ShortTrace(pin_b, self.p2)
circuit = current.circuit
circuit += c
return self
[docs]
class Part(jitx.Component):
"""Part component.
Args:
query: query to optionally override the current PartQuery from the DesignContext.
comp_name: name of the component to be created.
**kwargs: Additional query parameters
Raises:
Exception: If no components meet the requirements
"""
mpn: str
manufacturer: str
datasheet: str
reference_designator_prefix: str
landpattern: jitx.Landpattern
symbol: jitx.Symbol | dict[int | str, jitx.Symbol]
cmappings: list[jitx.PadMapping | jitx.SymbolMapping]
data: PartType
def __init__(
self,
query: PartQuery | None = None,
*,
comp_name: str | None = None,
**kwargs: Unpack[PartQueryDict],
):
rq = make_part_query(query, **kwargs)
try:
part = _internal_create_component(rq)
self.data = part
cc = part.component
_set_metadata(self, cc)
_set_component_class_name(self, cc)
for pin_prop in cc.pin_properties.pins:
_set_pin_on_object(self, pin_prop.pin, jitx.Port())
cc, self.landpattern = _build_landpattern(cc)
# Build symbol: use stdlib symbol for known passives, otherwise from data
if part.category == Category.RESISTOR:
from jitxlib.symbols.resistor import ResistorSymbol
self.symbol = ResistorSymbol()
elif part.category == Category.INDUCTOR:
from jitxlib.symbols.inductor import InductorSymbol
self.symbol = InductorSymbol()
elif part.category == Category.CAPACITOR:
from jitxlib.symbols.capacitor import CapacitorSymbol
self.symbol = CapacitorSymbol()
elif cc.symbols:
if len(cc.symbols) == 1:
self.symbol = create_dbsymbol(cc.symbols[0])
else:
self.symbol = {sc.bank: create_dbsymbol(sc) for sc in cc.symbols}
else:
raise ValueError("Component has no symbols")
self.cmappings = build_generic_mappings(
self, self.landpattern, self.symbol, cc
)
except Exception as e:
arg_list = "\n- ".join(f"{k}: {v}" for k, v in extract(rq).items())
logging.error(f"Failed to create part for query:\n- {arg_list}\n{e}")
raise
[docs]
def insert(
self,
pin_a: jitx.Port | jitx.Net,
pin_b: jitx.Port | jitx.Net,
*,
short_trace: TwoPinShortTrace | bool = TwoPinShortTrace.SHORT_TRACE_NEITHER,
) -> Self:
"""Insert this part between two pins of a circuit.
Args:
self: Component
pin_a: First pin
pin_b: Second pin
short_trace: Short trace option
Raises:
AssertionError: If the part does not have exactly 2 pins.
"""
c = InsertContainer()
ports = tuple(jitx.inspect.decompose(self, jitx.Port))
assert len(ports) == 2, "Part must have exactly 2 pins to be inserted."
p1, p2 = ports
c.nets = [
pin_a + p1,
pin_b + p2,
]
st = to_short_trace_enum(short_trace)
if (
st == TwoPinShortTrace.SHORT_TRACE_BOTH
or st == TwoPinShortTrace.SHORT_TRACE_ANODE
):
if not isinstance(pin_a, jitx.Port):
raise ValueError(
"Cannot make a shortrace with a net. Give a port to Part.insert's pin_a."
)
c.a_short_trace = jitx.net.ShortTrace(pin_a, p1)
if (
st == TwoPinShortTrace.SHORT_TRACE_BOTH
or st == TwoPinShortTrace.SHORT_TRACE_CATHODE
):
if not isinstance(pin_b, jitx.Port):
raise ValueError(
"Cannot make a shortrace with a net. Give a port to Part.insert's pin_b."
)
c.c_short_trace = jitx.net.ShortTrace(pin_b, p2)
circuit = current.circuit
circuit += c
return self
# Functions for searching components
[docs]
def search_resistors(
qb: PartQuery | None = None,
*,
limit: int = 1000,
**kwargs: Unpack[ResistorQueryDict],
) -> Sequence[PartJSON]:
"""Search for resistors.
Args:
qb: Query context
limit: Maximum number of results
**kwargs: Additional query parameters
Returns:
List of resistors
"""
rq = make_resistor_query(qb, **kwargs)
params = extract(rq)
results = dbquery(params, limit)
return results
[docs]
def search_capacitors(
qb: PartQuery | None = None,
*,
limit: int = 1000,
**kwargs: Unpack[CapacitorQueryDict],
) -> Sequence[PartJSON]:
"""Search for capacitors.
Args:
qb: Query context
limit: Maximum number of results
**kwargs: Additional query parameters
Returns:
List of capacitors
"""
cq = make_capacitor_query(qb, **kwargs)
params = extract(cq)
results = dbquery(params, limit)
return results
[docs]
def search_inductors(
qb: PartQuery | None = None,
*,
limit: int = 1000,
**kwargs: Unpack[InductorQueryDict],
) -> Sequence[PartJSON]:
"""Search for inductors.
Args:
qb: Query context
limit: Maximum number of results
**kwargs: Additional query parameters
Returns:
List of inductors
"""
iq = make_inductor_query(qb, **kwargs)
params = extract(iq)
results = dbquery(params, limit)
return results
[docs]
def search_parts(
qb: PartQuery | None = None,
*,
limit: int = 1000,
**kwargs: Unpack[PartQueryDict],
) -> Sequence[PartJSON]:
"""Search for parts.
Args:
qb: Query context
limit: Maximum number of results
**kwargs: Additional query parameters
Returns:
List of parts
"""
pq = make_part_query(qb, **kwargs)
params = extract(pq)
results = dbquery(params, limit)
return results
# Insert utility functions for two-pin components
[docs]
def to_short_trace_enum(short_trace: TwoPinShortTrace | bool) -> TwoPinShortTrace:
"""Convert a short trace parameter to a TwoPinShortTrace enum."""
# compatibility conversions for old interface
if isinstance(short_trace, bool):
return (
TwoPinShortTrace.SHORT_TRACE_ANODE
if short_trace
else TwoPinShortTrace.SHORT_TRACE_NEITHER
)
return short_trace
[docs]
def get_element_ports(inst: jitx.Component) -> tuple[jitx.Port, jitx.Port]:
"""Get the ports of an element."""
mAC = anode_cathode(inst)
if mAC:
return mAC
else:
a, b, *_ = jitx.inspect.decompose(inst, jitx.Port)
assert len(_) == 0, (
f"Expected passive components from the Parts DB to have two ports, got {a}, {b} and extra ports: {_}"
)
return a, b
[docs]
def anode_cathode(inst: jitx.Component) -> tuple[jitx.Port, jitx.Port] | None:
"""Get the anode and cathode ports of an element."""
a = getattr(inst, "a", None)
c = getattr(inst, "c", None)
if a and c:
return a, c