Strengthen pintable layout_rules skill and refresh empty extractions.
Treat PCB/application layout guidance as a first-class extract, keep those pages in PDF trim, and re-run IC extraction once when rules are still empty under older model_version. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,6 +4,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from packaging.version import Version
|
||||||
|
|
||||||
KNOWN_KINDS = frozenset({"decoupling_proximity", "thermal_via", "keepout", "length_match"})
|
KNOWN_KINDS = frozenset({"decoupling_proximity", "thermal_via", "keepout", "length_match"})
|
||||||
|
|
||||||
|
|
||||||
@@ -20,6 +22,37 @@ def _num(v: Any) -> float | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def has_any_layout_rule(raw: object) -> bool:
|
||||||
|
"""True when extraction already produced at least one structured rule."""
|
||||||
|
if not isinstance(raw, list):
|
||||||
|
return False
|
||||||
|
for row in raw:
|
||||||
|
if isinstance(row, dict) and str(row.get("kind") or "").strip() in KNOWN_KINDS:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def needs_layout_rules_refresh(
|
||||||
|
data: dict,
|
||||||
|
*,
|
||||||
|
min_scan_version: str,
|
||||||
|
) -> bool:
|
||||||
|
"""True when layout_rules are empty and the extract predates the scan version.
|
||||||
|
|
||||||
|
After a successful extract at ``min_scan_version`` or newer, an empty
|
||||||
|
``layout_rules`` list means the datasheet had no guidance — do not loop.
|
||||||
|
"""
|
||||||
|
if has_any_layout_rule(data.get("layout_rules")):
|
||||||
|
return False
|
||||||
|
ver = str(data.get("model_version") or "0.0.0")
|
||||||
|
if not min_scan_version or min_scan_version == "0.0.0":
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return Version(ver) < Version(min_scan_version)
|
||||||
|
except Exception:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def validate_layout_rules(raw: list | None) -> tuple[list[dict], list[str]]:
|
def validate_layout_rules(raw: list | None) -> tuple[list[dict], list[str]]:
|
||||||
"""Return (normalized rows, errors). Empty list is a valid skip."""
|
"""Return (normalized rows, errors). Empty list is a valid skip."""
|
||||||
if not raw:
|
if not raw:
|
||||||
|
|||||||
@@ -135,8 +135,36 @@ PINTABLE_TOOL = {
|
|||||||
},
|
},
|
||||||
"layout_rules": {
|
"layout_rules": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"description": "Optional PCB layout constraints from typical-application pages. kind must be decoupling_proximity, thermal_via, keepout, or length_match. max_distance_mm only if the PDF states a number — never invent 3 mm or 3W.",
|
"description": (
|
||||||
"items": {"type": "object"},
|
"PCB layout constraints from typical-application / PCB layout pages. "
|
||||||
|
"kind: decoupling_proximity | thermal_via | keepout | length_match. "
|
||||||
|
"Fields: pin, cap_value_hint, max_distance_mm (ONLY if the PDF states a "
|
||||||
|
"number — never invent 3 mm/JEDEC), same_layer (bool), min_via_count, "
|
||||||
|
"net_class, note, source_page. Empty array if the PDF has no layout guidance."
|
||||||
|
),
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"decoupling_proximity",
|
||||||
|
"thermal_via",
|
||||||
|
"keepout",
|
||||||
|
"length_match",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"pin": {"type": ["string", "null"]},
|
||||||
|
"cap_value_hint": {"type": ["string", "null"]},
|
||||||
|
"max_distance_mm": {"type": ["number", "null"]},
|
||||||
|
"same_layer": {"type": ["boolean", "null"]},
|
||||||
|
"min_via_count": {"type": ["integer", "null"]},
|
||||||
|
"net_class": {"type": ["string", "null"]},
|
||||||
|
"note": {"type": ["string", "null"]},
|
||||||
|
"source_page": {"type": ["integer", "null"]},
|
||||||
|
},
|
||||||
|
"required": ["kind"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"required": ["component_subtype", "component_subtype_description", "package_info", "pintable"],
|
"required": ["component_subtype", "component_subtype_description", "package_info", "pintable"],
|
||||||
@@ -257,11 +285,17 @@ _MAX_PDF_PAGES = 120
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Keywords used to find relevant pages for each extraction stage.
|
# Keywords used to find relevant pages for each extraction stage.
|
||||||
|
# Include PCB / typical-application pages so layout_rules can be extracted
|
||||||
|
# when large datasheets are trimmed to ≤_MAX_PDF_PAGES.
|
||||||
_PINTABLE_KEYWORDS = re.compile(
|
_PINTABLE_KEYWORDS = re.compile(
|
||||||
r"pin\s*(out|diagram|configuration|description|assignment|function|name|table|map)"
|
r"pin\s*(out|diagram|configuration|description|assignment|function|name|table|map)"
|
||||||
r"|ball\s*map|package\s*(pin|drawing|outline)|signal\s+description"
|
r"|ball\s*map|package\s*(pin|drawing|outline)|signal\s+description"
|
||||||
r"|absolute\s+maximum|recommended\s+operating|electrical\s+characteristics"
|
r"|absolute\s+maximum|recommended\s+operating|electrical\s+characteristics"
|
||||||
r"|ordering\s+information|device\s+information",
|
r"|ordering\s+information|device\s+information"
|
||||||
|
r"|pcb\s+layout|layout\s+(guideline|recommendation|consideration|hint)"
|
||||||
|
r"|typical\s+application|application\s+(circuit|schematic|information|note)"
|
||||||
|
r"|reference\s+design|decoupling|bypass\s+capacitor|thermal\s+via"
|
||||||
|
r"|land\s+pattern|keep[\s\-]?out|place\s+(close|near|within)",
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -596,7 +630,12 @@ async def extract_pintable(
|
|||||||
skill_name="extract-pintable",
|
skill_name="extract-pintable",
|
||||||
model=model,
|
model=model,
|
||||||
system=system,
|
system=system,
|
||||||
user_text=f"Extract pin table and package info for MPN: {mpn}",
|
user_text=(
|
||||||
|
f"Extract pin table, package info, absolute maximum ratings, "
|
||||||
|
f"and layout_rules (scan PCB layout / typical application / "
|
||||||
|
f"thermal pages; max_distance_mm only if the PDF states mm) "
|
||||||
|
f"for MPN: {mpn}"
|
||||||
|
),
|
||||||
pdf_path=trimmed,
|
pdf_path=trimmed,
|
||||||
output_tool=_to_tool(PINTABLE_TOOL),
|
output_tool=_to_tool(PINTABLE_TOOL),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -762,6 +762,10 @@ async def _stage_ic_extraction(ctx: PipelineContext) -> None:
|
|||||||
extracted_dir = ctx.ws.local_path("extracted")
|
extracted_dir = ctx.ws.local_path("extracted")
|
||||||
|
|
||||||
# Pre-categorize: workspace cache, library cache, or needs extraction
|
# Pre-categorize: workspace cache, library cache, or needs extraction
|
||||||
|
from backend.config import settings as app_settings
|
||||||
|
from backend.pinscopex.layout_rules import needs_layout_rules_refresh
|
||||||
|
|
||||||
|
layout_scan_ver = app_settings.get_default_model_version()
|
||||||
_ic_cache: dict[str, tuple] = {}
|
_ic_cache: dict[str, tuple] = {}
|
||||||
_ic_new_count = 0
|
_ic_new_count = 0
|
||||||
for mpn in ctx.ic_mpns:
|
for mpn in ctx.ic_mpns:
|
||||||
@@ -771,13 +775,27 @@ async def _stage_ic_extraction(ctx: PipelineContext) -> None:
|
|||||||
existing = json.loads(json_path.read_text())
|
existing = json.loads(json_path.read_text())
|
||||||
if existing.get("pintable"):
|
if existing.get("pintable"):
|
||||||
ws_ver = existing.get("model_version", "0.0.0")
|
ws_ver = existing.get("model_version", "0.0.0")
|
||||||
if not settings_svc.version_is_stale(ws_ver, ctx.min_ver):
|
if (
|
||||||
|
not settings_svc.version_is_stale(ws_ver, ctx.min_ver)
|
||||||
|
and not needs_layout_rules_refresh(
|
||||||
|
existing, min_scan_version=layout_scan_ver,
|
||||||
|
)
|
||||||
|
):
|
||||||
_ic_cache[mpn] = ("workspace",)
|
_ic_cache[mpn] = ("workspace",)
|
||||||
continue
|
continue
|
||||||
lib_key = proj_svc.library_has_extraction(ctx.storage, mpn, min_version=ctx.min_ver)
|
lib_key = proj_svc.library_has_extraction(ctx.storage, mpn, min_version=ctx.min_ver)
|
||||||
if lib_key:
|
if lib_key:
|
||||||
_ic_cache[mpn] = ("library", lib_key)
|
# Library hit may still lack layout_rules under the current skill.
|
||||||
continue
|
try:
|
||||||
|
lib_payload = ctx.storage.read_json(lib_key)
|
||||||
|
except Exception:
|
||||||
|
lib_payload = {}
|
||||||
|
if not needs_layout_rules_refresh(
|
||||||
|
lib_payload if isinstance(lib_payload, dict) else {},
|
||||||
|
min_scan_version=layout_scan_ver,
|
||||||
|
):
|
||||||
|
_ic_cache[mpn] = ("library", lib_key)
|
||||||
|
continue
|
||||||
_ic_new_count += 1
|
_ic_new_count += 1
|
||||||
|
|
||||||
broker.publish(ctx.project_id, "step_update",
|
broker.publish(ctx.project_id, "step_update",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"default_model_version": "1.9.0",
|
"default_model_version": "1.10.0",
|
||||||
"extract-pintable": {
|
"extract-pintable": {
|
||||||
"skill_id": "skill_01VMWPZuvuZAe4LmLbmsNWNY",
|
"skill_id": "skill_01VMWPZuvuZAe4LmLbmsNWNY",
|
||||||
"latest_version": "1784798970179642",
|
"latest_version": "1784798970179642",
|
||||||
|
|||||||
@@ -25,8 +25,9 @@ Fonte originale: canvas *Pinscope: crescita e DeepSeek*. Qui lo stato operativo.
|
|||||||
| Layout F1 | Domini / gruppi / satelliti | **Done** — `functional_groups.json` (no mm) |
|
| Layout F1 | Domini / gruppi / satelliti | **Done** — `functional_groups.json` (no mm) |
|
||||||
| Layout F1b | Pipeline Placement parallela | **Done** — API/UI `placement_*`, `placement_plan.json` |
|
| Layout F1b | Pipeline Placement parallela | **Done** — API/UI `placement_*`, `placement_plan.json` |
|
||||||
| Layout F2 | Placement IC packing mm | **Partial** — skeleton gated (`placement_pack.json`); no zone/export yet |
|
| Layout F2 | Placement IC packing mm | **Partial** — skeleton gated (`placement_pack.json`); no zone/export yet |
|
||||||
|
| C4 | `layout_rules` dal datasheet | **Improved** — skill 1.10.0 + trim keywords + cache refresh se vuoto |
|
||||||
|
|
||||||
**Done when (prossimo pacchetto):** smoke `--live` verde; hit_ratio visibile in UI logs; F2 packing oltre skeleton (collisioni, zone, export).
|
**Done when (prossimo pacchetto):** smoke `--live` verde; hit_ratio visibile in UI logs; F2 packing oltre skeleton (collisioni, zone, export); più IC library con `layout_rules` numerici.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,15 @@
|
|||||||
|
|
||||||
What's new in Pinscope.
|
What's new in Pinscope.
|
||||||
|
|
||||||
|
## 2.28.8 — 2026-09-13 — Stronger layout_rules extraction skill
|
||||||
|
|
||||||
|
Pintable skill now treats PCB / typical-application layout guidance as a first-class extract. Page trim keeps layout keywords; IC cache re-extracts once when `layout_rules` are empty under an older `model_version` (1.10.0+).
|
||||||
|
|
||||||
|
- [Changed] `skills/extract-pintable` — dedicated `layout_rules` step, examples, hard negatives; tighter `validate.py`.
|
||||||
|
- [Changed] Datasheet page trim includes PCB layout / decoupling / typical-application keywords.
|
||||||
|
- [Changed] Analysis IC extraction refreshes empties when `model_version` < default (1.10.0).
|
||||||
|
- [Changed] `default_model_version` → `1.10.0`.
|
||||||
|
|
||||||
## 2.28.7 — 2026-09-13 — Placement F2 pack skeleton (gated)
|
## 2.28.7 — 2026-09-13 — Placement F2 pack skeleton (gated)
|
||||||
|
|
||||||
Placement pipeline can propose satellite xy only when a `.kicad_pcb` layout exists and a `decoupling_proximity` rule has numeric `max_distance_mm`. No millimetres invented; otherwise `placement_pack.json` is skipped with an explicit reason.
|
Placement pipeline can propose satellite xy only when a `.kicad_pcb` layout exists and a `decoupling_proximity` rule has numeric `max_distance_mm`. No millimetres invented; otherwise `placement_pack.json` is skipped with an explicit reason.
|
||||||
|
|||||||
@@ -1,94 +1,163 @@
|
|||||||
---
|
---
|
||||||
skill_name: extract-pintable
|
skill_name: extract-pintable
|
||||||
description: Extract pin table, package info, and component subtype from an IC datasheet PDF. Returns structured data via the save_pintable tool.
|
description: Extract pin table, package info, absolute-maximum ratings, layout_rules, and component subtype from an IC datasheet PDF. Returns structured data via the save_pintable tool.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Extract Pin Table & Variant Info
|
# Extract Pin Table & Variant Info
|
||||||
|
|
||||||
Extract the pin table and variant/ordering information from a component datasheet and return it as structured JSON via the `save_pintable` tool.
|
Extract structured data from an IC datasheet and return it via the `save_pintable` tool.
|
||||||
|
|
||||||
|
**Priority order:** (1) complete pin table for the MPN package, (2) `layout_rules` from PCB / typical-application pages, (3) package + abs-max + subtype.
|
||||||
|
|
||||||
## Steps
|
## Steps
|
||||||
|
|
||||||
### 1. Read the datasheet PDF
|
### 1. Read the datasheet PDF
|
||||||
|
|
||||||
The datasheet PDF is provided in the user message. Focus on these sections:
|
Focus on these sections (figures count as evidence):
|
||||||
- **Pin configuration / pin assignment table** — This is the primary target. Look for tables listing pin number, pin name, description, and alternate functions.
|
- **Pin configuration / pin assignment table** — primary target
|
||||||
- **Ordering information / part number decoder** — Decode what each segment of the MPN means (package, temperature grade, packing, output voltage, etc.)
|
- **Ordering information / part number decoder**
|
||||||
- **Package information** — Pin count, package type (QFP, BGA, SOT-23, etc.)
|
- **Package information**
|
||||||
|
- **PCB layout / layout guidelines / land pattern notes**
|
||||||
|
- **Typical application / reference design** (placement callouts near caps, vias, keepouts)
|
||||||
|
- **Absolute maximum ratings**
|
||||||
|
|
||||||
### 2. Extract the pin table
|
### 2. Extract the pin table
|
||||||
|
|
||||||
For every pin on the component, extract:
|
For every pin:
|
||||||
- `number` (int or str) — The pin number, or BGA ball coordinate like `"A3"`
|
- `number` (int or str) — pin number, or BGA ball like `"A3"`
|
||||||
- `name` (str) — The pin name exactly as printed in the datasheet (e.g., `"VDD"`, `"PA0/SPI0_CLK"`)
|
- `name` (str) — verbatim from the datasheet (e.g. `"VDD"`, `"PA0/SPI0_CLK"`)
|
||||||
- `description` (str or null) — A brief description if the datasheet provides one
|
- `description` (str or null)
|
||||||
- `functions` (list[str] or null) — Alternate/multiplexed functions if the pin supports them
|
- `functions` (list[str] or null) — alternate/mux functions
|
||||||
|
|
||||||
Rules for pin extraction:
|
Rules:
|
||||||
- Include ALL pins — power, ground, NC, and signal pins
|
- Include ALL pins — power, ground, NC, exposed pad / EP
|
||||||
- Use pin names verbatim from the datasheet — do not rename or normalize
|
- Names verbatim — do not rename or normalize
|
||||||
- For multiplexed pins, put the primary name in `name` and alternates in `functions`
|
- Multiplexed pins: primary in `name`, alternates in `functions`
|
||||||
|
- If the datasheet has per-package tables, use the package matching the MPN
|
||||||
|
- Off-by-one pin numbers break everything downstream — double-check
|
||||||
|
|
||||||
Optional extras (omit if the PDF does not show them):
|
**Modules vs bare die (critical).** MPNs containing `WROOM`, `WROVER`, `MODULE`, `MOD-`, or `SIP` are *modules*. Extract the **module landing-pad table** (schematic pins). Do **not** extract the SoC/QFN ball map from a nested chip chapter.
|
||||||
|
- Espressif WROOM: pin 1 is GND. Pin 1 named `ANT`, `CHIP_PU`, or `XTAL_*` means you grabbed the die table — invalid.
|
||||||
|
- Crystal, RF antenna, and flash on a WROOM module are **inside the can**; they must not appear as schematic pin numbers.
|
||||||
|
|
||||||
|
Optional extras (omit if absent):
|
||||||
- `internal_features.pullup_pins` / `esd_clamp_pins` / `analog_switch` from the **block diagram** only.
|
- `internal_features.pullup_pins` / `esd_clamp_pins` / `analog_switch` from the **block diagram** only.
|
||||||
- `layout_rules` from **PCB layout / typical application** pages. `kind` is only `decoupling_proximity`, `thermal_via`, `keepout`, or `length_match` (intra-pair skew mm). Set `max_distance_mm` only when the document states a number — do not invent JEDEC or USB millimetres.
|
|
||||||
|
|
||||||
Rules for pin extraction:`
|
### 3. Extract layout_rules (required scan — empty OK)
|
||||||
- If the datasheet has separate tables for different packages, extract for the package matching the MPN
|
|
||||||
- Pay careful attention to pin numbering — off-by-one errors here break everything downstream
|
|
||||||
- **Modules vs bare die (critical).** MPNs containing `WROOM`, `WROVER`, `MODULE`, `MOD-`, or `SIP` are *modules*. Extract the **module landing-pad table** (connector pins the schematic uses). Do **not** extract the SoC/QFN ball map from a nested chip chapter or a sibling chip-only PDF.
|
|
||||||
- Espressif WROOM: pin 1 is GND (often a group of GND pads). Pin 1 named `ANT`, `CHIP_PU`, or `XTAL_*` means you grabbed the bare ESP32 die table — that will mark every module GND as “antenna shorted” and is invalid.
|
|
||||||
- Crystal, RF antenna, and flash on a WROOM module are **inside the can**; they must not appear as schematic pin numbers.
|
|
||||||
|
|
||||||
### 3. Extract package info
|
You **must** look for layout guidance. Emit `layout_rules` as a list. Use `[]` only after scanning layout / application / thermal pages and finding no placement guidance.
|
||||||
|
|
||||||
Decode the MPN and package details into a single `PackageInfo`:
|
#### Where to look
|
||||||
- `base_family` (str) — The base part family (e.g., `"MSPM0G3507"` from `"MSPM0G3507SPTR"`)
|
- Headings: “PCB Layout”, “Layout Guidelines”, “Layout Considerations”, “Board Layout”, “Land Pattern”
|
||||||
- `package` (str) — Package name (e.g., `"LQFP-48"`, `"SOT-23-5"`)
|
- “Typical Application”, “Application Circuit”, “Reference Design”
|
||||||
- `pin_count` (int) — Number of pins
|
- Thermal / EP / exposed-pad via recommendations
|
||||||
- `description` (str) — Human-readable decoding of the full MPN (e.g., `"MSPM0G3507, 48-pin LQFP, tape & reel"`)
|
- Callouts on application figures (“place CIN within 2 mm of VIN”)
|
||||||
|
|
||||||
Look for an "Ordering Information" or "Device Information" table in the datasheet — most datasheets have one.
|
#### Allowed `kind` (closed set)
|
||||||
|
| kind | Use when |
|
||||||
|
| --- | --- |
|
||||||
|
| `decoupling_proximity` | Bypass / decoupling / input / output cap near a supply or pin |
|
||||||
|
| `thermal_via` | Vias under exposed pad / thermal pad / EP |
|
||||||
|
| `keepout` | Keep foreign nets, digital return, or copper out of a region |
|
||||||
|
| `length_match` | Intra-pair skew / matched length limit in mm |
|
||||||
|
|
||||||
### 4. Extract absolute maximum ratings
|
#### Fields
|
||||||
|
- `pin` — number or name as printed (`"5"`, `"VIN"`, `"VDD"`, `"EP"`)
|
||||||
|
- `cap_value_hint` — only if shown (`"100nF"`, `"10µF"`)
|
||||||
|
- `max_distance_mm` — **number only if the PDF states millimetres**
|
||||||
|
- OK: “within 2 mm”, “< 5 mm”, “no more than 3 mm from the pin” → `2` / `5` / `3`
|
||||||
|
- NOT OK as a number: “as close as possible”, “close to the pin”, “adjacent”, “nearby” → set `max_distance_mm: null` and keep the rule with a `note`
|
||||||
|
- **Never invent** JEDEC, USB, IPC, or “standard 3 mm / 5 mm” distances
|
||||||
|
- `same_layer` — `true`/`false` only if text says same side / opposite side of the board; else null
|
||||||
|
- `min_via_count` — integer only if stated (“at least 4 vias”)
|
||||||
|
- `net_class` / `note` — short quote of the guidance
|
||||||
|
- `source_page` — 1-based page of the guidance (required when you emit a rule)
|
||||||
|
|
||||||
Copy the **Absolute Maximum Ratings** table (not Recommended Operating Conditions). For each row that a reviewer would need to compare against the schematic rails:
|
#### Examples
|
||||||
|
|
||||||
- `parameter` (str) — as printed (`VCC`, `VIN`, `I/O pin voltage`, `Storage temperature`, …)
|
Numeric proximity (copy the millimetre from the PDF):
|
||||||
- `min` / `max` (number or null) — numeric limit; omit the other side if the table only lists one
|
|
||||||
- `unit` (str) — `V`, `mA`, `°C`, …
|
|
||||||
- `source_page` (int) — 1-based datasheet page of that row
|
|
||||||
|
|
||||||
Include supply voltages, pin/input voltages, input current, and temperature. Skip ESD *human-body-model / IEC contact-discharge kV* rows unless they are the only voltage limit given. Do not invent numbers; if the table is a raster with no readable values, return an empty array.
|
```json
|
||||||
|
{
|
||||||
|
"kind": "decoupling_proximity",
|
||||||
|
"pin": "VIN",
|
||||||
|
"cap_value_hint": "10uF",
|
||||||
|
"max_distance_mm": 2.0,
|
||||||
|
"same_layer": true,
|
||||||
|
"note": "Place CIN within 2 mm of VIN",
|
||||||
|
"source_page": 14
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
**ESD / TVS / protection ICs (`ic.protection.esd` and similar):** also copy from Electrical Characteristics (not only abs-max):
|
Proximity without a millimetre (still emit the rule):
|
||||||
|
|
||||||
- Working / reverse working voltage **Vrwm** (or V_RWM / "operating voltage") as a **signed** min/max in volts — e.g. bidirectional ±13 V is `min: -13`, `max: 13`, `unit: "V"`.
|
```json
|
||||||
- One extra row whose `parameter` states polarity/topology as printed (`bidirectional`, `unidirectional`, `back-to-back`), `unit: "—"`, min/max omitted. Do **not** infer unidirectional from "IO" pins that list GND as the reference pin.
|
{
|
||||||
|
"kind": "decoupling_proximity",
|
||||||
|
"pin": "VDD",
|
||||||
|
"cap_value_hint": "100nF",
|
||||||
|
"max_distance_mm": null,
|
||||||
|
"note": "Place decoupling capacitor as close as possible to VDD",
|
||||||
|
"source_page": 22
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### 5. Assign component subtype (taxonomy)
|
Thermal vias:
|
||||||
|
|
||||||
The existing IC taxonomy subtypes are provided in the system prompt under `EXISTING IC TAXONOMY SUBTYPES`. Pick the best matching subtype based on the component's MPN, package info, and pin names.
|
```json
|
||||||
|
{
|
||||||
|
"kind": "thermal_via",
|
||||||
|
"pin": "EP",
|
||||||
|
"min_via_count": 4,
|
||||||
|
"note": "Use at least 4 thermal vias in the exposed pad",
|
||||||
|
"source_page": 18
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
If no existing subtype fits, propose a new one following the dot-notation convention (`ic.{category}.{specific}`).
|
#### Hard negatives
|
||||||
|
- Do not invent land-pattern pad sizes from the mechanical drawing alone
|
||||||
|
- Do not emit `length_match` for USB/HDMI/PCIe unless the **this** datasheet states a skew/length number
|
||||||
|
- Do not use kinds outside the closed set
|
||||||
|
- One rule per distinct pin/guidance; prefer supply pins that show caps in the application figure
|
||||||
|
|
||||||
Set the chosen subtype on the `component_subtype` field.
|
### 4. Extract package info
|
||||||
|
|
||||||
### 6. Quality checks
|
- `base_family` — e.g. `"MSPM0G3507"` from `"MSPM0G3507SPTR"`
|
||||||
|
- `package` — e.g. `"LQFP-48"`, `"SOT-23-5"`
|
||||||
|
- `pin_count` (int)
|
||||||
|
- `description` — human-readable MPN decode
|
||||||
|
|
||||||
Before producing output, verify:
|
Prefer “Ordering Information” / “Device Information” tables.
|
||||||
- Pin count matches what the datasheet says for this package
|
|
||||||
- No duplicate pin numbers
|
|
||||||
- No pins are missing (compare against the datasheet's stated pin count)
|
|
||||||
- Pin names look reasonable (not garbled OCR artifacts)
|
|
||||||
|
|
||||||
### 7. Validate and output
|
### 5. Extract absolute maximum ratings
|
||||||
|
|
||||||
Validate your extraction against the output schema:
|
Copy the **Absolute Maximum Ratings** table (not Recommended Operating Conditions):
|
||||||
|
|
||||||
|
- `parameter`, `min` / `max`, `unit`, `source_page` (1-based)
|
||||||
|
|
||||||
|
Include supply voltages, pin/input voltages, input current, temperature. Skip HBM/IEC kV ESD rows unless they are the only voltage limit. Do not invent numbers.
|
||||||
|
|
||||||
|
**ESD / TVS (`ic.protection.esd` and similar):** also from Electrical Characteristics:
|
||||||
|
- Vrwm / operating voltage as signed min/max in volts
|
||||||
|
- One row for polarity/topology as printed (`bidirectional`, …), `unit: "—"`
|
||||||
|
|
||||||
|
### 6. Assign component subtype
|
||||||
|
|
||||||
|
Pick the best dotted subtype from `EXISTING IC TAXONOMY SUBTYPES` (e.g. `ic.mcu`, `ic.power.ldo`). If none fit, propose `ic.{category}.{specific}`.
|
||||||
|
|
||||||
|
### 7. Quality checks
|
||||||
|
|
||||||
|
Before output:
|
||||||
|
- Pin count matches the package for this MPN
|
||||||
|
- No duplicate / missing pin numbers
|
||||||
|
- `layout_rules` scanned (list present; `[]` only if truly no guidance)
|
||||||
|
- Every emitted rule has a valid `kind`; every numeric `max_distance_mm` comes from the PDF text/figure
|
||||||
|
- Pin names are not OCR garbage
|
||||||
|
|
||||||
|
### 8. Validate and output
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 /skills/extract-pintable/validate.py '<your JSON here>'
|
python3 /skills/extract-pintable/validate.py '<your JSON here>'
|
||||||
```
|
```
|
||||||
|
|
||||||
If validation passes, call the `save_pintable` tool with the structured result.
|
If validation passes, call `save_pintable`. Do NOT write files to disk — use the tool.
|
||||||
Do NOT write files to disk — use the tool.
|
|
||||||
|
|||||||
@@ -56,7 +56,30 @@
|
|||||||
},
|
},
|
||||||
"layout_rules": {
|
"layout_rules": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {"type": "object"}
|
"description": "PCB layout constraints from typical-application / PCB layout pages. Empty if none stated.",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"decoupling_proximity",
|
||||||
|
"thermal_via",
|
||||||
|
"keepout",
|
||||||
|
"length_match"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pin": {"type": ["string", "null"]},
|
||||||
|
"cap_value_hint": {"type": ["string", "null"]},
|
||||||
|
"max_distance_mm": {"type": ["number", "null"]},
|
||||||
|
"same_layer": {"type": ["boolean", "null"]},
|
||||||
|
"min_via_count": {"type": ["integer", "null"]},
|
||||||
|
"net_class": {"type": ["string", "null"]},
|
||||||
|
"note": {"type": ["string", "null"]},
|
||||||
|
"source_page": {"type": ["integer", "null"]}
|
||||||
|
},
|
||||||
|
"required": ["kind"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["component_subtype", "package_info", "pintable"]
|
"required": ["component_subtype", "package_info", "pintable"]
|
||||||
|
|||||||
@@ -93,6 +93,62 @@ def validate(data: dict) -> list[str]:
|
|||||||
kind = row.get("kind")
|
kind = row.get("kind")
|
||||||
if kind not in kinds:
|
if kind not in kinds:
|
||||||
errors.append(f"layout_rules[{i}] unknown kind: {kind!r}")
|
errors.append(f"layout_rules[{i}] unknown kind: {kind!r}")
|
||||||
|
continue
|
||||||
|
dist = row.get("max_distance_mm")
|
||||||
|
if dist is not None and dist is not False:
|
||||||
|
if isinstance(dist, bool):
|
||||||
|
errors.append(
|
||||||
|
f"layout_rules[{i}].max_distance_mm must be a number or null"
|
||||||
|
)
|
||||||
|
elif isinstance(dist, (int, float)):
|
||||||
|
if float(dist) <= 0:
|
||||||
|
errors.append(
|
||||||
|
f"layout_rules[{i}].max_distance_mm must be > 0"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
v = float(str(dist).strip())
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
errors.append(
|
||||||
|
f"layout_rules[{i}].max_distance_mm must be numeric "
|
||||||
|
f"or null (got {dist!r}) — do not invent distances; "
|
||||||
|
f"use null when the PDF only says 'close'"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if v <= 0:
|
||||||
|
errors.append(
|
||||||
|
f"layout_rules[{i}].max_distance_mm must be > 0"
|
||||||
|
)
|
||||||
|
via = row.get("min_via_count")
|
||||||
|
if via is not None and via is not False and not isinstance(via, bool):
|
||||||
|
if isinstance(via, int):
|
||||||
|
if via <= 0:
|
||||||
|
errors.append(
|
||||||
|
f"layout_rules[{i}].min_via_count must be > 0"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
iv = int(float(str(via).strip()))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
errors.append(
|
||||||
|
f"layout_rules[{i}].min_via_count must be an integer "
|
||||||
|
f"or null (got {via!r})"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if iv <= 0:
|
||||||
|
errors.append(
|
||||||
|
f"layout_rules[{i}].min_via_count must be > 0"
|
||||||
|
)
|
||||||
|
page = row.get("source_page")
|
||||||
|
if page is not None and not isinstance(page, int):
|
||||||
|
errors.append(
|
||||||
|
f"layout_rules[{i}].source_page must be an integer or null"
|
||||||
|
)
|
||||||
|
same = row.get("same_layer")
|
||||||
|
if same is not None and not isinstance(same, bool):
|
||||||
|
errors.append(
|
||||||
|
f"layout_rules[{i}].same_layer must be a boolean or null"
|
||||||
|
)
|
||||||
|
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,26 @@ def test_empty_list_is_explicit_skip():
|
|||||||
assert errors == []
|
assert errors == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_needs_refresh_when_empty_and_old_version():
|
||||||
|
from backend.pinscopex.layout_rules import needs_layout_rules_refresh
|
||||||
|
|
||||||
|
assert needs_layout_rules_refresh(
|
||||||
|
{"model_version": "1.9.0", "layout_rules": []},
|
||||||
|
min_scan_version="1.10.0",
|
||||||
|
)
|
||||||
|
assert not needs_layout_rules_refresh(
|
||||||
|
{"model_version": "1.10.0", "layout_rules": []},
|
||||||
|
min_scan_version="1.10.0",
|
||||||
|
)
|
||||||
|
assert not needs_layout_rules_refresh(
|
||||||
|
{
|
||||||
|
"model_version": "1.9.0",
|
||||||
|
"layout_rules": [{"kind": "decoupling_proximity", "max_distance_mm": None}],
|
||||||
|
},
|
||||||
|
min_scan_version="1.10.0",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_unknown_kind_rejected_and_non_numeric_distance_is_null():
|
def test_unknown_kind_rejected_and_non_numeric_distance_is_null():
|
||||||
ok, errors = validate_layout_rules([
|
ok, errors = validate_layout_rules([
|
||||||
{"kind": "not_a_kind", "max_distance_mm": 1.0},
|
{"kind": "not_a_kind", "max_distance_mm": 1.0},
|
||||||
|
|||||||
Reference in New Issue
Block a user