Allow ferrite beads without impedance_ohm to load.
Missing Z is optional, not invented. graph_build skips bad models instead of aborting. Other inductors still require value_henries.
This commit is contained in:
@@ -7,6 +7,7 @@ in ``periscope/dependency/`` until this module is the live winner.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -27,6 +28,8 @@ from backend.periscopex.parsers import parse_bom, parse_netlist_any
|
|||||||
from backend.periscopex.resolve_passives import SkippedItem, resolve_bom, resolved_to_specs
|
from backend.periscopex.resolve_passives import SkippedItem, resolve_bom, resolved_to_specs
|
||||||
from backend.periscopex.utils import safe_mpn
|
from backend.periscopex.utils import safe_mpn
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Ref-des prefix → type. Longer prefixes must be checked first (LED before L).
|
# Ref-des prefix → type. Longer prefixes must be checked first (LED before L).
|
||||||
_REF_PREFIXES: tuple[tuple[str, ComponentType], ...] = (
|
_REF_PREFIXES: tuple[tuple[str, ComponentType], ...] = (
|
||||||
("LED", ComponentType.DISCRETE),
|
("LED", ComponentType.DISCRETE),
|
||||||
@@ -160,8 +163,12 @@ def _load_component_models(directory: str | Path) -> dict[str, ComponentSpecs]:
|
|||||||
if not dirpath.is_dir():
|
if not dirpath.is_dir():
|
||||||
return result
|
return result
|
||||||
for json_file in dirpath.glob("*.json"):
|
for json_file in dirpath.glob("*.json"):
|
||||||
raw = json.loads(json_file.read_text())
|
try:
|
||||||
model = ComponentModel.model_validate(raw)
|
raw = json.loads(json_file.read_text())
|
||||||
|
model = ComponentModel.model_validate(raw)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("skipping bad component model %s", json_file)
|
||||||
|
continue
|
||||||
result[model.mpn] = model.specs
|
result[model.mpn] = model.specs
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -163,8 +163,8 @@ class InductorSpecs(BaseModel):
|
|||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def _require_primary_value(self) -> InductorSpecs:
|
def _require_primary_value(self) -> InductorSpecs:
|
||||||
if self.component_subtype == "passive.ferrite_bead":
|
if self.component_subtype == "passive.ferrite_bead":
|
||||||
if self.impedance_ohm is None:
|
# Z at test frequency is optional. Missing Z is INSUFFICIENT
|
||||||
raise ValueError("ferrite bead requires impedance_ohm")
|
# evidence later — never invent impedance_ohm, never crash ingest.
|
||||||
return self
|
return self
|
||||||
if self.value_henries is None:
|
if self.value_henries is None:
|
||||||
raise ValueError("inductor requires value_henries")
|
raise ValueError("inductor requires value_henries")
|
||||||
|
|||||||
@@ -175,16 +175,19 @@ def simple_to_typed_passive_specs(simple: SimpleComponentSpecs) -> ComponentSpec
|
|||||||
)
|
)
|
||||||
if subtype == "passive.ferrite_bead":
|
if subtype == "passive.ferrite_bead":
|
||||||
raw = vals.get("impedance_ohm") or vals.get("value_ohms")
|
raw = vals.get("impedance_ohm") or vals.get("value_ohms")
|
||||||
if raw is None:
|
z = None
|
||||||
raise ValueError("Missing impedance_ohm in auto-resolved ferrite bead specs")
|
if raw is not None:
|
||||||
z = _parse_spice_value(str(raw)) if isinstance(raw, str) else float(raw)
|
z = _parse_spice_value(str(raw)) if isinstance(raw, str) else float(raw)
|
||||||
dcr_raw = vals.get("dcr_ohms")
|
dcr_raw = vals.get("dcr_ohms")
|
||||||
dcr = None
|
dcr = None
|
||||||
if dcr_raw is not None:
|
if dcr_raw is not None:
|
||||||
dcr = _parse_spice_value(str(dcr_raw)) if isinstance(dcr_raw, str) else float(dcr_raw)
|
dcr = _parse_spice_value(str(dcr_raw)) if isinstance(dcr_raw, str) else float(dcr_raw)
|
||||||
|
formatted = value_formatted
|
||||||
|
if not formatted and z is not None:
|
||||||
|
formatted = _format_value(z, "ohm")
|
||||||
return InductorSpecs(
|
return InductorSpecs(
|
||||||
component_subtype=st, value_henries=None,
|
component_subtype=st, value_henries=None,
|
||||||
value_formatted=value_formatted or _format_value(z, "ohm"),
|
value_formatted=formatted or "FB",
|
||||||
tolerance=tolerance, package=package,
|
tolerance=tolerance, package=package,
|
||||||
current_rating_a=str(vals.get("current_rating_a")) if vals.get("current_rating_a") else None,
|
current_rating_a=str(vals.get("current_rating_a")) if vals.get("current_rating_a") else None,
|
||||||
dcr_ohms=dcr, impedance_ohm=z,
|
dcr_ohms=dcr, impedance_ohm=z,
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
What's new in Periscope.
|
What's new in Periscope.
|
||||||
|
|
||||||
|
## 2.60.1 — 2026-09-20 — Ferrite bead without Z still loads
|
||||||
|
|
||||||
|
Ferrite beads that only have current/DCR validate. Missing `impedance_ohm` is not invented and does not crash `graph_build`. Other inductors still need `value_henries`.
|
||||||
|
|
||||||
|
- [Fixed] `InductorSpecs` no longer requires Z on `passive.ferrite_bead`.
|
||||||
|
- [Changed] Bad `models/*.json` are skipped with a log, not a pipeline abort.
|
||||||
|
|
||||||
## 2.60.0 — 2026-09-20 — Incomplete PCB exam and owner project delete
|
## 2.60.0 — 2026-09-20 — Incomplete PCB exam and owner project delete
|
||||||
|
|
||||||
Unfinished placing/routing still uses MODE=pcb. Parser and checks run on partial layouts; missing copper is INSUFFICIENT or skip, never invented millimetres. Via≠Pad unchanged. Dashboard owners can delete a project after a confirm dialog.
|
Unfinished placing/routing still uses MODE=pcb. Parser and checks run on partial layouts; missing copper is INSUFFICIENT or skip, never invented millimetres. Via≠Pad unchanged. Dashboard owners can delete a project after a confirm dialog.
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
"""Ferrite beads without Z validate; other inductors still need henries."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
from backend.periscopex.graph import _load_component_models
|
||||||
|
from backend.periscopex.models import ComponentModel, InductorSpecs
|
||||||
|
from backend.periscopex.resolve_passives import simple_to_typed_passive_specs
|
||||||
|
from backend.periscopex.models import SimpleComponentSpecs
|
||||||
|
|
||||||
|
|
||||||
|
def test_ferrite_bead_without_impedance_ohm_validates():
|
||||||
|
specs = InductorSpecs(
|
||||||
|
component_subtype="passive.ferrite_bead",
|
||||||
|
value_formatted="120 ohm @ 100 MHz",
|
||||||
|
current_rating_a="3A",
|
||||||
|
dcr_ohms=0.03,
|
||||||
|
)
|
||||||
|
assert specs.impedance_ohm is None
|
||||||
|
model = ComponentModel(mpn="BLM21PG121SN1D", specs=specs)
|
||||||
|
assert model.specs.impedance_ohm is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_ferrite_bead_live_payload_without_z_validates():
|
||||||
|
model = ComponentModel.model_validate({
|
||||||
|
"mpn": "BLM21PG121SN1D",
|
||||||
|
"specs": {
|
||||||
|
"specs_type": "inductor",
|
||||||
|
"component_subtype": "passive.ferrite_bead",
|
||||||
|
"value_henries": 120.0,
|
||||||
|
"value_formatted": "120 ohm @ 100 MHz",
|
||||||
|
"tolerance": None,
|
||||||
|
"package": "0805",
|
||||||
|
"current_rating_a": "3A",
|
||||||
|
"dcr_ohms": 0.03,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert model.specs.impedance_ohm is None
|
||||||
|
assert model.specs.current_rating_a == "3A"
|
||||||
|
assert model.specs.dcr_ohms == 0.03
|
||||||
|
|
||||||
|
|
||||||
|
def test_ferrite_bead_with_impedance_ohm_still_ok():
|
||||||
|
specs = InductorSpecs(
|
||||||
|
component_subtype="passive.ferrite_bead",
|
||||||
|
value_formatted="120 ohm @ 100 MHz",
|
||||||
|
impedance_ohm=120.0,
|
||||||
|
current_rating_a="3A",
|
||||||
|
dcr_ohms=0.03,
|
||||||
|
)
|
||||||
|
assert specs.impedance_ohm == 120.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_inductor_still_requires_value_henries():
|
||||||
|
with pytest.raises(ValidationError, match="inductor requires value_henries"):
|
||||||
|
InductorSpecs(
|
||||||
|
component_subtype="passive.inductor",
|
||||||
|
value_formatted="15nH",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_models_bead_without_z_does_not_raise(tmp_path: Path):
|
||||||
|
payload = {
|
||||||
|
"mpn": "BLM21PG121SN1D",
|
||||||
|
"specs": {
|
||||||
|
"specs_type": "inductor",
|
||||||
|
"component_subtype": "passive.ferrite_bead",
|
||||||
|
"value_formatted": "FB",
|
||||||
|
"current_rating_a": "3A",
|
||||||
|
"dcr_ohms": 0.03,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
(tmp_path / "BLM21PG121SN1D.json").write_text(json.dumps(payload) + "\n")
|
||||||
|
loaded = _load_component_models(tmp_path)
|
||||||
|
assert "BLM21PG121SN1D" in loaded
|
||||||
|
assert loaded["BLM21PG121SN1D"].impedance_ohm is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_models_skips_inductor_missing_henries(tmp_path: Path):
|
||||||
|
bad = {
|
||||||
|
"mpn": "LQW-BAD",
|
||||||
|
"specs": {
|
||||||
|
"specs_type": "inductor",
|
||||||
|
"component_subtype": "passive.inductor",
|
||||||
|
"value_formatted": "x",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
(tmp_path / "LQW-BAD.json").write_text(json.dumps(bad) + "\n")
|
||||||
|
loaded = _load_component_models(tmp_path)
|
||||||
|
assert loaded == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_simple_to_typed_bead_without_z():
|
||||||
|
simple = SimpleComponentSpecs(
|
||||||
|
specs_type="passive",
|
||||||
|
component_subtype="passive.ferrite_bead",
|
||||||
|
values={"current_rating_a": "3A", "dcr_ohms": 0.03},
|
||||||
|
)
|
||||||
|
specs = simple_to_typed_passive_specs(simple)
|
||||||
|
assert specs.specs_type == "inductor"
|
||||||
|
assert specs.impedance_ohm is None
|
||||||
|
assert specs.dcr_ohms == 0.03
|
||||||
Reference in New Issue
Block a user