# Piano di implementazione: analisi AF della traccia (v1) DRC = KiCad. Periscope = analisi. **Stesura ora = Python.** Niente rustup, Auth/JWT, OpenEMS, FEM 3D, field solver 2.5D. Pad ≠ via ≠ track ≠ zone. Non si inventano Z, I, mm, εr, tr, f. Michele 2026-09-22: piano ok con correzioni sotto. SI/HF 2.62 e `PE-AF-001` restano **additivi**. ImpedenceFinder vendored (`vendor/impedancefinder`) non si riscrive. Repo: `~/Development/periscope`. Copia di questo file anche in `docs/piano-af-analisi.md`. --- ## Paletti obbligatori (Michele) 1. **Z target solo da datasheet** (`z0_ohm` / `zdiff_ohm` / finestra `z_min`–`z_max` in `layout_rules`). Manca → **skip del confronto Z**, finding visibile INSUFFICIENT, **niente 50/90 Ω**. 2. **Coppia differenziale: i due membri insieme** (Zdiff, accoppiamento intra-coppia, lunghezze/skew). Mai una traccia sola se esiste il partner `_P/_N` o `+/-` sulla board. 3. **Simulazione numerica sì, OpenEMS no.** Niente FEM 3D né solver di campo 2.5D della spec. Metodo contenuto: sezioni da ImpedenceFinder Z0(x) → RLGC lossless per sezione → cascade ABCD → S11/S21 **solo se** Z_ref (= target datasheet) è FACT. 4. **C++ vs Rust:** valutati in §15. Implementazione v1 in Python. Niente rustup. 5. **Skip AF non silenziosi.** In report: *«Pista ad alta frequenza non controllata per mancanza di …»* (tr, f, εr, stackup, Z target datasheet, span via). Finding **INSUFFICIENT / REVIEW**. Non INFO con Z/I inventati. **Se il trigger è falso** (calcolato con FACT e `l` sotto soglia) → **niente finding** AF su quella net/coppia. --- ## 1. Perimetro vs spec La spec (`specifica-af.md`) descrive Gerber/ODB++, field solver 2.5D, S/TDR completi. Periscope esamina `.kicad_pcb` + grafo + libreria. | Spec | v1 Periscope | | :--- | :--- | | Parser Gerber/ODB++ | No. KiCad `LayoutGraph`. | | Trigger λ/10 e tr/(6 tpd) | Sì, FACT only. | | Gomiti, taper, 3W, piano 3H | Sì, solo net/coppie **triggerate**. Non DRC. | | Via stub / L_via | Sì se drill + **span layer** + h. Altrimenti PE-AF-002 span via. | | Field solver 2.5D / OpenEMS | **Fuori.** | | RLGC + S-param | Cascade sezioni lossless + Z0 ImpedenceFinder. S11/S21 se Z_ref datasheet. | | TDR IFFT | Fuori. Profilo Z0(x) non si chiama TDR. | | Wheeler allegato B | Non usato. Hammerstad/Cohn del vendor. | `run_pcb_checks` (SI/HF) **invariato**. --- ## 2. Trigger Candidati: net con rame **traccia**, non power/GND, non `skip_si_net` (I2C/GPIO/EN/CC/strap), non XTAL. Coppia = un’unità: `l = max(l_p, l_n)`; se un membro è candidato, lo è la coppia. Costanti: c = 2.99792458×10⁸ m/s. `tpd = √εr_eff / c`. `λ = c / (f √εr_eff)`. - λ/10 se `f` FACT oppure `f = 0.35/tr` con `tr` FACT, e εr_eff da stackup+geometria (ImpedenceFinder). - Rise time: `l ≥ tr / (6 tpd)` con `tr` FACT (non la forma tr·vp/2). **Trigger vero** → analisi discontinuità / Z / cascade / via. **Trigger falso** (entrambe le soglie calcolate e `l` sotto) → silenzio AF. **Trigger non calcolabile** su un candidato (manca tr **e** f, o manca stackup/εr) → **PE-AF-002 visibile**, testo italiano con la lista di ciò che manca. Non è “trigger falso”. --- ## 3. Skip visibili (non silenziosi) | Manca | Testo (es.) | Quando | | :--- | :--- | :--- | | stackup / εr | mancanza di stackup, εr | candidato, trigger non valutabile | | tr, f | mancanza di tr, f | candidato, serve almeno uno | | Z target datasheet | mancanza di Z target datasheet | trigger **vero**, niente confronto ±10% e niente S | | span via | mancanza di span via | trigger vero **e** c’è un via sulla net/coppia senza layers | Niente ohm, niente ampere, niente 1 ns USB. Status WARNING, class REVIEW, `evidence_status=INSUFFICIENT` (il clamp del motore finding non deve promuovere RULE/ERROR). `PE-AF-001` (AI Z0 senza stackup) resta; è già INSUFFICIENT visibile. --- ## 4. Coppia differenziale Partner: ImpedenceFinder + `partner_net`. Un finding per coppia, non due cloni. Zdiff da `diff_microstrip_z0` / `diff_stripline_z0`. Intra-coppia: lunghezze, non regola 3W (il mate non è aggressore). 3W solo vs **altre** net. --- ## 5. Simulazione numerica (non OpenEMS) Per net/coppia triggerata con campioni Z0: 1. Sezioni di lunghezza campionata, Z0 ImpedenceFinder (flag `plane_broken` esclusi dal voto ohm). 2. Lossless: `L' = Z0 · tpd`, `C' = tpd / Z0` per metro. 3. ABCD di cascata; S11/S21 rispetto a **Z_ref = target datasheet**. 4. Confronto ΔZ0 ±10% vs stessa finestra datasheet. Duplicato `PE-SI-002`: non secondo FAIL. Senza Z_ref: PE-AF-002, cascade non pubblica S. --- ## 6. Dati - Stackup KiCad già parsato (`epsilon_r`, thickness; mai 1 oz default). - `LayoutVia.layers` da `(layers "F.Cu" "B.Cu")`. Size anulare ≠ drill ≠ track. - `tr`/`f` da `layout_rules` del driver sulla net (`rise_time`, `tr_ns`, `f_hz`, …) e `net_class` espanso come SI. --- ## 7. Finding PE-* | ID | Ruolo | | :--- | :--- | | PE-AF-001 | AI Z0 senza evidenza (esistente) | | PE-AF-002 | Skip visibile “non controllata per mancanza di …” | | PE-AF-020 | Gomito ~90° | | PE-AF-021 | Gradino W / taper | | PE-AF-030 | `plane_broken` | | PE-AF-031 | Return 3H | | PE-AF-032 | `plane_split_nearby` | | PE-AF-040 | 3W vs aggressore (non il mate) | | PE-AF-050 | Z0/Zdiff vs finestra datasheet + cascade in `calculation` | | PE-AF-051 | CPWG/unknown: no ohm | | PE-AF-060 | L_via se drill+h+span | | PE-AF-061 | Via stub vs λ/20 | --- ## 8. Moduli e pipeline `af_trigger.py`, `af_rlgc.py`, `af_trace_check.py`. Dopo `run_pcb_checks`, prima `af_ai`. Non dentro la lista 2.62. Stage SSE `af_trace`. --- ## 9. Test HubAudio Path opzionale `~/Development/HubAudio/.../HubAudio.kicad_pcb`. USB senza tr FACT → PE-AF-002 (tr/f), zero 90 Ω. GPIO → niente AF. Trigger sintetico corto con tr FACT → zero finding. Lunga + target datasheet → PE-AF-050. Coppia: un finding, non D+ solo. SI stub `PE-SI-007` resta. --- ## 10. Fasi v1 (questa implementazione) Parser via layers; trigger; skip visibili; coppia; Z/cascade se target; gomiti/width; 3W; via se span; pipeline; changelog; pytest. --- ## 11. Fuori scope OpenEMS, FEM, 2.5D, TDR IFFT, NEXT/FEXT in volt, 5W default, auto-miter sul PCB, Gerber/ODB++, Auth/JWT, rustup. --- ## 15. C++ vs Rust (valutazione; stesura Python) Criteri (come `docs/rust-criteri.md`): geometria pad≠via≠track≠zone al confine; memoria/ownership su HubAudio; hot path profilato; determinismo; FFI grosso (struct in → struct out). | Criterio | AF v1 (trigger + Shapely + cascade) | C++ | Rust | | :--- | :--- | :--- | :--- | | Geometria | Tipi già distinti in Python; Shapely nel vendor. Un port deve **non** fondere via/pad. | Possibile (CGAL/Clipper), confine pericoloso se si “semplifica” AABB. | Stesso rischio; ownership aiuta i buffer, non la semantica PCB. | | Memoria | HubAudio è un `.kicad_pcb` grande; il costo oggi è parse + zone Shapely, non la cascade (O(sezioni)). Nessuna misura che Python non basti. | Arena/SoA utili dopo profilo. | Stesso, dopo misura. | | Hot path | Non profilato. Candidati *futuri* restano parser PCB e point-in-poly, non PE-AF isolato. | Solo se il profilo lo dice. | Idem. | | Determinismo | Cascade ABCD e Z0 chiusi sono deterministici. | ok | ok (niente HashMap nel testo finding). | | FFI grosso | Un rewrite ora = due runtime + duplicare LayoutGraph. **Complicherebbe** (Michele). | pybind/nanobind, grosso se si passa tutta la board. | PyO3, stesso costo. ImpedenceFinder resterebbe Python/Shapely. | **Decisione:** v1 Python. Nessun pezzo scelto per C++ o Rust. Michele può volere Rust un giorno; sequenza obbligatoria resta: Python corretto → misura HubAudio → profilo → scelta esplicita. ImpedenceFinder non si riscrive “per Rust”. OpenEMS non è candidato a nessun linguaggio in Periscope.