Files
periscope/docs/piano-af-analisi.md
T
michele c87adf11a9 Add AF trace analysis after PCB checks (2.63.5).
Trigger λ/10 and tr/(6 tpd) from FACT; pairs analyzed together; Z only from
datasheet; visible INSUFFICIENT skips; lossless RLGC cascade (no OpenEMS).
2026-09-22 08:39:37 +02:00

7.7 KiB
Raw Blame History

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_minz_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 = ununità: 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.