Recover ferrite bead Z from datasheet evidence without aborting graph_build.
This commit is contained in:
@@ -158,9 +158,9 @@ 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:
|
from backend.periscopex.ferrite_z import recover_bead_specs
|
||||||
raise ValueError("ferrite bead requires impedance_ohm")
|
|
||||||
return self
|
return recover_bead_specs(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")
|
||||||
return self
|
return self
|
||||||
|
|||||||
@@ -306,19 +306,21 @@ 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:
|
impedance_ohm = None
|
||||||
raise ValueError("Missing impedance_ohm in auto-resolved ferrite bead specs")
|
if raw is not None:
|
||||||
impedance_ohm = _parse_spice_value(str(raw)) if isinstance(raw, str) else float(raw)
|
impedance_ohm = _parse_spice_value(str(raw)) if isinstance(raw, str) else float(raw)
|
||||||
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_raw = vals.get("dcr_ohms")
|
dcr_raw = vals.get("dcr_ohms")
|
||||||
dcr_ohms: float | None = None
|
dcr_ohms: float | None = None
|
||||||
if dcr_raw is not None:
|
if dcr_raw is not None:
|
||||||
dcr_ohms = _parse_spice_value(str(dcr_raw)) if isinstance(dcr_raw, str) else float(dcr_raw)
|
dcr_ohms = _parse_spice_value(str(dcr_raw)) if isinstance(dcr_raw, str) else float(dcr_raw)
|
||||||
formatted = value_formatted or _format_value(impedance_ohm, "ohm")
|
formatted = value_formatted
|
||||||
|
if not formatted and impedance_ohm is not None:
|
||||||
|
formatted = _format_value(impedance_ohm, "ohm")
|
||||||
return InductorSpecs(
|
return InductorSpecs(
|
||||||
component_subtype=subtype_for_specs,
|
component_subtype=subtype_for_specs,
|
||||||
value_henries=None,
|
value_henries=None,
|
||||||
value_formatted=formatted,
|
value_formatted=formatted or "FB",
|
||||||
tolerance=tolerance,
|
tolerance=tolerance,
|
||||||
package=package,
|
package=package,
|
||||||
current_rating_a=current_rating_a,
|
current_rating_a=current_rating_a,
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
"""Ferrite-bead impedance from stated evidence only.
|
||||||
|
|
||||||
|
Never invent Z from MPN codes. Prefer datasheet text, then a quoted
|
||||||
|
``N ohm @ freq`` value string. Do not copy ohms into henries.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from backend.periscopex.utils import safe_mpn
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from backend.periscopex.models import InductorSpecs
|
||||||
|
|
||||||
|
# 120 ohm @ 100 MHz | 120Ω at 100MHz
|
||||||
|
_OHM_AT = re.compile(
|
||||||
|
r"(?P<z>\d+(?:\.\d+)?)\s*(?P<k>k)?\s*(?:ohms?|Ω|R)\s*"
|
||||||
|
r"(?:@|at|/)\s*(?P<freq>\d+(?:\.\d+)?)\s*(?P<fun>kHz|MHz|GHz|Hz)",
|
||||||
|
re.I,
|
||||||
|
)
|
||||||
|
# Impedance (at 100MHz): 120 ohm
|
||||||
|
_IMP_TABLE = re.compile(
|
||||||
|
r"impedance.{0,120}?(?P<freq>\d+(?:\.\d+)?)\s*(?P<fun>kHz|MHz|GHz|Hz)"
|
||||||
|
r".{0,80}?(?P<z>\d+(?:\.\d+)?)\s*(?:k)?\s*(?:ohms?|Ω)",
|
||||||
|
re.I | re.S,
|
||||||
|
)
|
||||||
|
_DCR = re.compile(r"\bdcr\b|dc\s*resistance", re.I)
|
||||||
|
|
||||||
|
|
||||||
|
def _z_float(num: str, k: str | None) -> float:
|
||||||
|
z = float(num)
|
||||||
|
if k:
|
||||||
|
z *= 1e3
|
||||||
|
return z
|
||||||
|
|
||||||
|
|
||||||
|
def parse_quoted_z(text: str) -> tuple[float, str] | None:
|
||||||
|
"""Return (ohm, 'N ohm @ freq') if the text states Z at a frequency."""
|
||||||
|
if not (text or "").strip():
|
||||||
|
return None
|
||||||
|
blob = text.replace("\u00a0", " ")
|
||||||
|
hits: list[tuple[float, str, float]] = []
|
||||||
|
for m in _OHM_AT.finditer(blob):
|
||||||
|
start = max(0, m.start() - 40)
|
||||||
|
if _DCR.search(blob[start:m.start()]):
|
||||||
|
continue
|
||||||
|
z = _z_float(m.group("z"), m.group("k"))
|
||||||
|
freq = f"{m.group('freq')} {m.group('fun')}"
|
||||||
|
mhz = _to_mhz(m.group("freq"), m.group("fun"))
|
||||||
|
hits.append((z, f"{z:g} ohm @ {freq}", mhz))
|
||||||
|
for m in _IMP_TABLE.finditer(blob):
|
||||||
|
z = float(m.group("z"))
|
||||||
|
freq = f"{m.group('freq')} {m.group('fun')}"
|
||||||
|
mhz = _to_mhz(m.group("freq"), m.group("fun"))
|
||||||
|
hits.append((z, f"{z:g} ohm @ {freq}", mhz))
|
||||||
|
if not hits:
|
||||||
|
return None
|
||||||
|
hits.sort(key=lambda h: abs(h[2] - 100.0))
|
||||||
|
z, formatted, _ = hits[0]
|
||||||
|
return z, formatted
|
||||||
|
|
||||||
|
|
||||||
|
def _to_mhz(num: str, unit: str) -> float:
|
||||||
|
n = float(num)
|
||||||
|
u = unit.lower()
|
||||||
|
if u == "hz":
|
||||||
|
return n / 1e6
|
||||||
|
if u == "khz":
|
||||||
|
return n / 1e3
|
||||||
|
if u == "ghz":
|
||||||
|
return n * 1e3
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
def z_from_datasheet_text(text: str) -> tuple[float, str] | None:
|
||||||
|
hit = parse_quoted_z(text)
|
||||||
|
if hit is None:
|
||||||
|
return None
|
||||||
|
z, formatted = hit
|
||||||
|
return z, f"{formatted} (datasheet)"
|
||||||
|
|
||||||
|
|
||||||
|
def z_from_pdf(path: Path | str, *, max_chars: int = 80_000) -> tuple[float, str] | None:
|
||||||
|
pdf = Path(path)
|
||||||
|
if not pdf.is_file():
|
||||||
|
return None
|
||||||
|
from backend.periscopex.pdf_text import extract_pdf_document_text
|
||||||
|
|
||||||
|
text = extract_pdf_document_text(pdf, max_chars=max_chars)
|
||||||
|
return z_from_datasheet_text(text)
|
||||||
|
|
||||||
|
|
||||||
|
def find_mpn_pdf(mpn: str, *dirs: Path | str) -> Path | None:
|
||||||
|
safe = safe_mpn(mpn)
|
||||||
|
names = (f"{safe}.pdf", f"{mpn}.pdf")
|
||||||
|
for d in dirs:
|
||||||
|
root = Path(d)
|
||||||
|
if not root.is_dir():
|
||||||
|
continue
|
||||||
|
for name in names:
|
||||||
|
p = root / name
|
||||||
|
if p.is_file():
|
||||||
|
return p
|
||||||
|
for p in root.glob("*.pdf"):
|
||||||
|
if safe.lower() in p.stem.lower() or mpn.lower() in p.stem.lower():
|
||||||
|
return p
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def recover_bead_specs(
|
||||||
|
specs: InductorSpecs,
|
||||||
|
*,
|
||||||
|
extra_text: str = "",
|
||||||
|
pdf_path: Path | str | None = None,
|
||||||
|
) -> InductorSpecs:
|
||||||
|
"""Fill impedance_ohm from quoted/PDF evidence. Never invent. Unstuff henries."""
|
||||||
|
z = specs.impedance_ohm
|
||||||
|
formatted = specs.value_formatted or ""
|
||||||
|
source = extra_text
|
||||||
|
if z is None:
|
||||||
|
quoted = parse_quoted_z(formatted) or parse_quoted_z(extra_text)
|
||||||
|
if quoted is not None:
|
||||||
|
z, fmt = quoted
|
||||||
|
if "ohm" not in formatted.lower() and "Ω" not in formatted:
|
||||||
|
formatted = f"{fmt} (value string)"
|
||||||
|
else:
|
||||||
|
formatted = fmt if "ohm" in fmt.lower() else formatted
|
||||||
|
elif pdf_path is not None:
|
||||||
|
pdf_hit = z_from_pdf(pdf_path)
|
||||||
|
if pdf_hit is not None:
|
||||||
|
z, formatted = pdf_hit
|
||||||
|
elif extra_text:
|
||||||
|
ds = z_from_datasheet_text(extra_text)
|
||||||
|
if ds is not None:
|
||||||
|
z, formatted = ds
|
||||||
|
source = extra_text
|
||||||
|
|
||||||
|
henries = specs.value_henries
|
||||||
|
if z is not None and henries is not None and abs(henries - z) < 1e-6:
|
||||||
|
if parse_quoted_z(formatted) or parse_quoted_z(source):
|
||||||
|
henries = None
|
||||||
|
|
||||||
|
if z == specs.impedance_ohm and henries == specs.value_henries and formatted == specs.value_formatted:
|
||||||
|
return specs
|
||||||
|
specs.impedance_ohm = z
|
||||||
|
specs.value_henries = henries
|
||||||
|
specs.value_formatted = formatted or specs.value_formatted
|
||||||
|
return specs
|
||||||
@@ -169,7 +169,26 @@ def _load_component_models(directory: str | Path) -> dict[str, ComponentSpecs]:
|
|||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("skipping bad component model %s", json_file)
|
logger.exception("skipping bad component model %s", json_file)
|
||||||
continue
|
continue
|
||||||
result[model.mpn] = model.specs
|
specs = model.specs
|
||||||
|
if getattr(specs, "component_subtype", None) == "passive.ferrite_bead":
|
||||||
|
from backend.periscopex.ferrite_z import find_mpn_pdf, recover_bead_specs
|
||||||
|
|
||||||
|
pdf = find_mpn_pdf(
|
||||||
|
model.mpn,
|
||||||
|
dirpath.parent / "uploads" / "datasheets",
|
||||||
|
dirpath.parent / "datasheets",
|
||||||
|
)
|
||||||
|
filled = recover_bead_specs(specs, extra_text=json.dumps(raw), pdf_path=pdf)
|
||||||
|
if (
|
||||||
|
filled.impedance_ohm != getattr(specs, "impedance_ohm", None)
|
||||||
|
or filled.value_henries != getattr(specs, "value_henries", None)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
_save_component_model(model.mpn, filled, dirpath)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("could not persist recovered bead Z for %s", model.mpn)
|
||||||
|
specs = filled
|
||||||
|
result[model.mpn] = specs
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -163,9 +163,9 @@ 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":
|
||||||
# Z at test frequency is optional. Missing Z is INSUFFICIENT
|
from backend.periscopex.ferrite_z import recover_bead_specs
|
||||||
# evidence later — never invent impedance_ohm, never crash ingest.
|
|
||||||
return self
|
return recover_bead_specs(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")
|
||||||
return self
|
return self
|
||||||
|
|||||||
@@ -185,13 +185,15 @@ def simple_to_typed_passive_specs(simple: SimpleComponentSpecs) -> ComponentSpec
|
|||||||
formatted = value_formatted
|
formatted = value_formatted
|
||||||
if not formatted and z is not None:
|
if not formatted and z is not None:
|
||||||
formatted = _format_value(z, "ohm")
|
formatted = _format_value(z, "ohm")
|
||||||
return InductorSpecs(
|
spec = InductorSpecs(
|
||||||
component_subtype=st, value_henries=None,
|
component_subtype=st, value_henries=None,
|
||||||
value_formatted=formatted or "FB",
|
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,
|
||||||
)
|
)
|
||||||
|
from backend.periscopex.ferrite_z import recover_bead_specs
|
||||||
|
return recover_bead_specs(spec, extra_text=" ".join(str(v) for v in vals.values() if v is not None))
|
||||||
if subtype.startswith("passive.inductor"):
|
if subtype.startswith("passive.inductor"):
|
||||||
raw = vals.get("value_henries")
|
raw = vals.get("value_henries")
|
||||||
if raw is None:
|
if raw is None:
|
||||||
|
|||||||
@@ -902,6 +902,23 @@ async def extract_specs(
|
|||||||
pintable=result.get("pintable", []),
|
pintable=result.get("pintable", []),
|
||||||
package_info=result.get("package_info"),
|
package_info=result.get("package_info"),
|
||||||
)
|
)
|
||||||
|
if component_type == "passive":
|
||||||
|
from backend.periscopex.ferrite_z import recover_bead_specs
|
||||||
|
from backend.periscopex.resolve_passives import simple_to_typed_passive_specs
|
||||||
|
|
||||||
|
try:
|
||||||
|
typed = simple_to_typed_passive_specs(specs)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
typed = specs
|
||||||
|
else:
|
||||||
|
if getattr(typed, "component_subtype", None) == "passive.ferrite_bead":
|
||||||
|
typed = recover_bead_specs(
|
||||||
|
typed,
|
||||||
|
extra_text=json.dumps(filtered_values),
|
||||||
|
pdf_path=pdf_path,
|
||||||
|
)
|
||||||
|
model_obj = ComponentModel(mpn=mpn, specs=typed)
|
||||||
|
else:
|
||||||
model_obj = ComponentModel(mpn=mpn, specs=specs)
|
model_obj = ComponentModel(mpn=mpn, specs=specs)
|
||||||
|
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -1114,8 +1131,15 @@ async def auto_resolve_specs(
|
|||||||
|
|
||||||
# Convert passive SimpleComponentSpecs to typed models
|
# Convert passive SimpleComponentSpecs to typed models
|
||||||
if component_type == "passive":
|
if component_type == "passive":
|
||||||
|
from backend.periscopex.ferrite_z import recover_bead_specs
|
||||||
from backend.periscopex.resolve_passives import simple_to_typed_passive_specs
|
from backend.periscopex.resolve_passives import simple_to_typed_passive_specs
|
||||||
|
|
||||||
typed = simple_to_typed_passive_specs(specs)
|
typed = simple_to_typed_passive_specs(specs)
|
||||||
|
if getattr(typed, "component_subtype", None) == "passive.ferrite_bead":
|
||||||
|
extra = f"{digikey_description}\n" + "\n".join(
|
||||||
|
f"{p.get('name')}: {p.get('value')}" for p in digikey_params
|
||||||
|
)
|
||||||
|
typed = recover_bead_specs(typed, extra_text=extra)
|
||||||
return ComponentModel(mpn=mpn, specs=typed)
|
return ComponentModel(mpn=mpn, specs=typed)
|
||||||
|
|
||||||
return ComponentModel(mpn=mpn, specs=specs)
|
return ComponentModel(mpn=mpn, specs=specs)
|
||||||
@@ -1286,8 +1310,12 @@ async def resolve_from_value(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if component_type == "passive":
|
if component_type == "passive":
|
||||||
|
from backend.periscopex.ferrite_z import recover_bead_specs
|
||||||
from backend.periscopex.resolve_passives import simple_to_typed_passive_specs
|
from backend.periscopex.resolve_passives import simple_to_typed_passive_specs
|
||||||
|
|
||||||
typed = simple_to_typed_passive_specs(specs)
|
typed = simple_to_typed_passive_specs(specs)
|
||||||
|
if getattr(typed, "component_subtype", None) == "passive.ferrite_bead":
|
||||||
|
typed = recover_bead_specs(typed, extra_text=value)
|
||||||
return ComponentModel(mpn=mpn, specs=typed)
|
return ComponentModel(mpn=mpn, specs=typed)
|
||||||
|
|
||||||
return ComponentModel(mpn=mpn, specs=specs)
|
return ComponentModel(mpn=mpn, specs=specs)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"default_model_version": "1.14.0",
|
"default_model_version": "1.15.0",
|
||||||
"extract-pintable": {
|
"extract-pintable": {
|
||||||
"skill_id": "skill_01VMWPZuvuZAe4LmLbmsNWNY",
|
"skill_id": "skill_01VMWPZuvuZAe4LmLbmsNWNY",
|
||||||
"latest_version": "1784798970179642",
|
"latest_version": "1784798970179642",
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
What's new in Periscope.
|
What's new in Periscope.
|
||||||
|
|
||||||
|
## 2.60.2 — 2026-09-20 — Ferrite Z from datasheet, no graph_build abort
|
||||||
|
|
||||||
|
Beads with an MPN pull `impedance_ohm` from the datasheet or a quoted `N ohm @ freq` string. Missing Z after that is INSUFFICIENT, not a crash. Ohms are never written to `value_henries`. Leftover dependency validator no longer requires Z.
|
||||||
|
|
||||||
|
- [Fixed] `graph_build` loads DCR-only beads; leftover `ferrite bead requires impedance_ohm` removed.
|
||||||
|
- [Changed] Ferrite Z recovered from datasheet PDF / quoted value; extract-specs skill 1.15.0.
|
||||||
|
|
||||||
## 2.60.1 — 2026-09-20 — Ferrite bead without Z still loads
|
## 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`.
|
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`.
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ Use SPICE prefixes with units: `T` `G` `M` `k` `m` `u` `n` `p`. Good: `"30V"`, `
|
|||||||
|
|
||||||
Do not infer or calculate. If min/typ/max all matter, include them in the string (`"550mV typ, 850mV max"`). Extra keys are discarded.
|
Do not infer or calculate. If min/typ/max all matter, include them in the string (`"550mV typ, 850mV max"`). Extra keys are discarded.
|
||||||
|
|
||||||
|
Ferrite beads (`passive.ferrite_bead`): put Z at the test frequency in `impedance_ohm` (e.g. 120 Ω @ 100 MHz). Never write that impedance into `value_henries`. If the datasheet does not state impedance, omit `impedance_ohm`.
|
||||||
|
|
||||||
## save_specs payload
|
## save_specs payload
|
||||||
|
|
||||||
- `component_subtype` — dotted path (`discrete.diode.schottky`)
|
- `component_subtype` — dotted path (`discrete.diode.schottky`)
|
||||||
|
|||||||
@@ -143,9 +143,8 @@
|
|||||||
"extra_specs": [
|
"extra_specs": [
|
||||||
{
|
{
|
||||||
"name": "impedance_ohm",
|
"name": "impedance_ohm",
|
||||||
"description": "Impedance at the stated test frequency",
|
"description": "Impedance at the stated test frequency (do not invent; omit if the datasheet has no Z)",
|
||||||
"unit": "ohm",
|
"unit": "ohm"
|
||||||
"required": true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "current_rating_a",
|
"name": "current_rating_a",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Ferrite beads without Z validate; other inductors still need henries."""
|
"""Ferrite beads: recover Z from evidence; never invent; never abort ingest."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -8,25 +8,26 @@ from pathlib import Path
|
|||||||
import pytest
|
import pytest
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
from backend.periscopex.ferrite_z import recover_bead_specs, z_from_datasheet_text
|
||||||
from backend.periscopex.graph import _load_component_models
|
from backend.periscopex.graph import _load_component_models
|
||||||
from backend.periscopex.models import ComponentModel, InductorSpecs
|
from backend.periscopex.models import ComponentModel, InductorSpecs, SimpleComponentSpecs
|
||||||
from backend.periscopex.resolve_passives import simple_to_typed_passive_specs
|
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():
|
def test_ferrite_bead_quoted_ohm_fills_z_not_henries():
|
||||||
specs = InductorSpecs(
|
specs = InductorSpecs(
|
||||||
component_subtype="passive.ferrite_bead",
|
component_subtype="passive.ferrite_bead",
|
||||||
value_formatted="120 ohm @ 100 MHz",
|
value_formatted="120 ohm @ 100 MHz",
|
||||||
current_rating_a="3A",
|
current_rating_a="3A",
|
||||||
dcr_ohms=0.03,
|
dcr_ohms=0.03,
|
||||||
)
|
)
|
||||||
assert specs.impedance_ohm is None
|
assert specs.impedance_ohm == 120.0
|
||||||
|
assert specs.value_henries is None
|
||||||
model = ComponentModel(mpn="BLM21PG121SN1D", specs=specs)
|
model = ComponentModel(mpn="BLM21PG121SN1D", specs=specs)
|
||||||
assert model.specs.impedance_ohm is None
|
assert model.specs.impedance_ohm == 120.0
|
||||||
|
|
||||||
|
|
||||||
def test_ferrite_bead_live_payload_without_z_validates():
|
def test_ferrite_bead_live_payload_fills_z_from_formatted():
|
||||||
model = ComponentModel.model_validate({
|
model = ComponentModel.model_validate({
|
||||||
"mpn": "BLM21PG121SN1D",
|
"mpn": "BLM21PG121SN1D",
|
||||||
"specs": {
|
"specs": {
|
||||||
@@ -40,7 +41,8 @@ def test_ferrite_bead_live_payload_without_z_validates():
|
|||||||
"dcr_ohms": 0.03,
|
"dcr_ohms": 0.03,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
assert model.specs.impedance_ohm is None
|
assert model.specs.impedance_ohm == 120.0
|
||||||
|
assert model.specs.value_henries is None
|
||||||
assert model.specs.current_rating_a == "3A"
|
assert model.specs.current_rating_a == "3A"
|
||||||
assert model.specs.dcr_ohms == 0.03
|
assert model.specs.dcr_ohms == 0.03
|
||||||
|
|
||||||
@@ -54,6 +56,7 @@ def test_ferrite_bead_with_impedance_ohm_still_ok():
|
|||||||
dcr_ohms=0.03,
|
dcr_ohms=0.03,
|
||||||
)
|
)
|
||||||
assert specs.impedance_ohm == 120.0
|
assert specs.impedance_ohm == 120.0
|
||||||
|
assert specs.value_henries is None
|
||||||
|
|
||||||
|
|
||||||
def test_inductor_still_requires_value_henries():
|
def test_inductor_still_requires_value_henries():
|
||||||
@@ -105,3 +108,42 @@ def test_simple_to_typed_bead_without_z():
|
|||||||
assert specs.specs_type == "inductor"
|
assert specs.specs_type == "inductor"
|
||||||
assert specs.impedance_ohm is None
|
assert specs.impedance_ohm is None
|
||||||
assert specs.dcr_ohms == 0.03
|
assert specs.dcr_ohms == 0.03
|
||||||
|
|
||||||
|
|
||||||
|
def test_bead_mpn_datasheet_text_fills_z():
|
||||||
|
text = (
|
||||||
|
"BLM21PG121SN1D chip ferrite bead\n"
|
||||||
|
"Impedance (at 100MHz / 20°C) 120 ohm\n"
|
||||||
|
"Rated current 3A\n"
|
||||||
|
"DC resistance 0.03 ohm\n"
|
||||||
|
)
|
||||||
|
hit = z_from_datasheet_text(text)
|
||||||
|
assert hit is not None
|
||||||
|
z, formatted = hit
|
||||||
|
assert z == 120.0
|
||||||
|
assert "100" in formatted
|
||||||
|
assert "datasheet" in formatted
|
||||||
|
specs = InductorSpecs(
|
||||||
|
component_subtype="passive.ferrite_bead",
|
||||||
|
value_formatted="FB",
|
||||||
|
current_rating_a="3A",
|
||||||
|
dcr_ohms=0.03,
|
||||||
|
)
|
||||||
|
filled = recover_bead_specs(specs, extra_text=text)
|
||||||
|
assert filled.impedance_ohm == 120.0
|
||||||
|
assert filled.value_henries is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_bead_mpn_datasheet_without_z_does_not_invent():
|
||||||
|
text = "BLM21PG121SN1D\nRated current 3 A\nDCR 0.03 ohm max\nPackage 0805"
|
||||||
|
assert z_from_datasheet_text(text) is None
|
||||||
|
specs = InductorSpecs(
|
||||||
|
component_subtype="passive.ferrite_bead",
|
||||||
|
value_formatted="FB",
|
||||||
|
current_rating_a="3A",
|
||||||
|
dcr_ohms=0.03,
|
||||||
|
)
|
||||||
|
filled = recover_bead_specs(specs, extra_text=text)
|
||||||
|
assert filled.impedance_ohm is None
|
||||||
|
model = ComponentModel(mpn="BLM21PG121SN1D", specs=filled)
|
||||||
|
assert model.specs.impedance_ohm is None
|
||||||
|
|||||||
@@ -61,6 +61,6 @@ def test_skills_manifest_bumped_for_prompt_change():
|
|||||||
text = (ROOT / "periscope" / "src" / "backend" / "skills_manifest.json").read_text(
|
text = (ROOT / "periscope" / "src" / "backend" / "skills_manifest.json").read_text(
|
||||||
encoding="utf-8"
|
encoding="utf-8"
|
||||||
)
|
)
|
||||||
assert '"default_model_version": "1.14.0"' in text
|
assert '"default_model_version": "1.15.0"' in text
|
||||||
validate = load_skill_validator("extract-pintable")
|
validate = load_skill_validator("extract-pintable")
|
||||||
assert validate is not None
|
assert validate is not None
|
||||||
|
|||||||
Reference in New Issue
Block a user