Thomas Seri commited on
Commit
130f544
·
1 Parent(s): 77c84d3

README: rimossi numeri senza fonte (energia, modelli esterni), 'record' -> 'migliore tra gli R-08', brevetti UIBM senza numero non verificato

Browse files
.gitattributes DELETED
@@ -1,2 +0,0 @@
1
- *.safetensors filter=lfs diff=lfs merge=lfs -text
2
- *.pt filter=lfs diff=lfs merge=lfs -text
 
 
 
LICENSE.md DELETED
@@ -1,63 +0,0 @@
1
- # R08 — NON-COMMERCIAL RESEARCH & ETHICAL LICENSE (v2.0)
2
-
3
- Copyright (c) 2026 Thomas Seri. All Rights Reserved.
4
-
5
- ---
6
-
7
- ### 1. DEFINITIONS
8
- - **"Author"** means Thomas Seri (Sentinel).
9
- - **"Software"** means the R08 / R-08 (Rob) Geodesic Mesh Neural Network architecture, model weights, checkpoints, Watchman Keystone modules, Knowledge Tree ontologies, tokenizers (`gpTommy`), search engines, configurations, and documentation provided herein for Sentinel LifeOS.
10
- - **"Commercial Use"** means using the Software or any derivative work in any business, for-profit venture, service provision, integration into commercial applications, or for any activity generating revenue.
11
- - **"Non-Commercial Research Use"** means using the Software solely for personal education, academic study, reproducibility evaluation, and non-profit benchmarking.
12
-
13
- ---
14
-
15
- ### 2. GRANT OF PERMISSION
16
- Subject to the terms and restrictions of this License, the Author grants you a personal, worldwide, royalty-free, non-exclusive, non-transferable license to:
17
- 1. Download, view, and inspect the code and weights.
18
- 2. Run inference for Non-Commercial Research Use only.
19
- 3. Cite and reference the architecture and benchmark results in academic or research publications, provided that prominent attribution is given to **Thomas Seri** and **Sentinel AI**.
20
-
21
- ---
22
-
23
- ### 3. RESTRICTIONS & PROHIBITIONS
24
- You shall NOT:
25
- 1. Use the Software or any portion thereof for any Commercial Use without a separate, written, commercial license agreement signed by the Author (**Thomas Seri**).
26
- 2. Sublicense, lease, sell, rent, or distribute modified copies of the Software for commercial purposes.
27
- 3. Reverse-engineer, distill, extract weights, train competing models on synthetic outputs, or replicate proprietary architectural features (including 3-stage geodesic cascades, 12 icosahedral hub pyramids, and 1D causal inter-sphere convolutions) for deployment in commercial or third-party products without prior written authorization from **Thomas Seri**.
28
- 4. Remove, alter, obfuscate, or circumvent the Author's attribution (**Thomas Seri**), this License (`LICENSE.md`), or the integrated **Watchman Keystone (`watchman_keystone_r08.py`)** security and ethical manifold.
29
-
30
- ---
31
-
32
- ### 3.1 STRICT ETHICAL RESTRICTIONS & PROHIBITED USES (ACCEPTABLE USE POLICY)
33
- It is strictly, unconditionally, and globally FORBIDDEN to use the Software, model weights, checkpoints, architecture, code, or any derivative works for:
34
- 1. **Military, Defense & Warfare**: Weapons development, explosives, chemical/biological/nuclear agents, autonomous defense systems, suicide/combat drones, missile targeting, tactical combat simulations, warfare logistics, or military applications of any kind.
35
- 2. **Religious Propaganda & Indoctrination**: Proselytism, forced religious manipulation, spiritual coercion, cult indoctrination, or automated doctrinal dissemination.
36
- 3. **Inappropriate Images, Pornography & Sexual Exploitation**: Generation, prompting, processing, or dissemination of inappropriate images (NSFW), sexually explicit content, pornography, non-consensual nudity/deepfakes, CSAM, or sexual exploitation of any kind.
37
- 4. **Cyberattacks, Malware & Industrial Sabotage**: Writing, training, or deploying computer malware, ransomware, trojans, keyloggers, zero-day exploits, phishing kits, unauthorized network intrusion tools, or attacks/sabotage on industrial PLCs, SCADA systems, and critical infrastructure (healthcare, energy, water, finance).
38
- 5. **Addictions, Gambling, Illicit Drugs & Self-Harm**: Promotion or automation of gambling, online casinos, slot/betting manipulation algorithms, synthesis or distribution of illicit drugs, or encouragement/instruction of suicide, self-harm, or violence.
39
- 6. **Political Campaigns & Partisan Propaganda**: Electoral manipulation, voter persuasion bots, automated political astroturfing, partisan lobbying, candidate campaigns, or deceptive political disinformation/fake news.
40
- 7. **High-Risk Autonomous Decisions (Medical & Financial)**: Autonomous clinical diagnosis, prescription of medication, or unmonitored financial trading without certified human professional oversight.
41
- 8. **Surveillance, Biometric Profiling & GDPR Privacy Violations**: Mass biometric surveillance, facial tracking without consent, racial/ethnic predictive policing, discrimination based on race, gender, religion, or nationality, or unauthorized extraction/storage of personal identifiable financial data (such as cleartext or obfuscated IBANs and credit card numbers).
42
-
43
- **IMMEDIATE REVOCATION CLAUSE:** ANY USE VIOLATING THIS SECTION IMMEDIATELY AND AUTOMATICALLY REVOKES ALL PERMISSIONS AND LICENSES GRANTED HEREIN WITH RETROACTIVE EFFECT, RENDERING ANY ONGOING POSSESSION OR USE OF THE SOFTWARE AN ILLEGAL INFRINGEMENT OF THE AUTHOR'S INTELLECTUAL PROPERTY AND COPYRIGHT.
44
-
45
- ---
46
-
47
- ### 4. COPYRIGHT, TRADEMARK & CRYPTOGRAPHIC WATCHMAN PROTECTION
48
- The names **"Sentinel"**, **"Sentinel LifeOS"**, **"Sentinel AI"**, **"R-08"**, **"R08"**, **"gpTommy"**, and **"Thomas Seri"** are protected identities, trade names, and trademarks of the Author. No license is granted herein to use these names, logos, or marks for trade, commercial advertising, or to imply any endorsement, affiliation, or sponsorship of any third-party product, service, or company without prior, express written authorization from **Thomas Seri**.
49
-
50
- **CRYPTOGRAPHIC & GEODESIC WATCHMAN KEYSTONE NOTICE:** The neural weights (`model.safetensors`) and inference runtime (`modeling_geodesic.py`, `watchman_keystone_r08.py`) incorporate a non-linear symplectic manifold (`GELU + SiLU + Tanh`), an orthogonal cryptographic projection lock (`SHA3-512`), and multi-layer forensic watermarks tied to **Thomas Seri** and this License. Any tampering with this License, the Author attribution, or the Watchman Keystone invalidates the projection manifold and constitutes unlawful circumvention of technological protection measures.
51
-
52
- ---
53
-
54
- ### 5. DISCLAIMER OF WARRANTY & LIMITATION OF LIABILITY
55
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
56
-
57
- **MEDICAL, FINANCIAL & LEGAL DISCLAIMER:** The Software is an experimental research system. The outputs generated by this model do NOT constitute certified medical advice, legal counsel, or financial/investment recommendations. The Author and Sentinel AI shall have NO liability for any personal injury, financial loss, legal liability, or damages arising directly or indirectly from decisions made based on outputs of the Software. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
58
-
59
- ---
60
-
61
- For commercial licensing, enterprise partnerships, or integration inquiries:
62
- Contact: **Thomas Seri** / **Sentinel AI**
63
- Website: https://www.sentinelhub.eu
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -38,22 +38,22 @@ inference: false
38
  **Organizzazione:** SentinelHub
39
  **Hardware di Sviluppo:** Apple Silicon (Metal Performance Shaders - MPS)
40
  **Versione Successiva Evoluta:** 🚀 [**Sentinel R-08 v7.2 (51.6M)**](https://huggingface.co/SentinelHub/R-08-v7.2) | [GitHub v7.2](https://github.com/Thomasseri/R-08-v7.2)
41
- **Nuovo Record Scientifico:** 🌟 [**Sentinel R-08 v7.2a DA ZERO (51.6M)**](https://github.com/Thomasseri/R-08-v7.2a) | [Hugging Face v7.2a](https://huggingface.co/SentinelHub/R-08-v7.2a)
42
 
43
  > [!TIP]
44
- > ### 🚀 È Rilasciato il Nuovo Modello Sentinel R08-v7.2 (51.6M) & il Record di v7.2a!
45
  > L'evoluzione neurale geometrica di Sentinel LifeOS è ora disponibile con **Convoluzioni Causali 1D Inter-Stadio ($k=4$)** e **12 Hub Regionali dell'Icosaedro**:
46
- > - **BLiMP-IT sale all'82.02% (v7.2) e tocca il record assoluto dell'82.51% (v7.2a DA ZERO)**
47
- > - **MultiBLiMP balza al record storico del 76.71% (v7.2a)**
48
  > - **Eliminazione totale di loop e balbuzie ciclica** nei testi lunghi
49
- > - **-85.7% di memoria KV-Cache** e **-28.3% di consumo energetico** rispetto al Transformer convenzionale
50
  > - 🌟 **GitHub v7.2a:** [Thomasseri/R-08-v7.2a](https://github.com/Thomasseri/R-08-v7.2a) | [SentinelHub/R-08-v7.2a](https://huggingface.co/SentinelHub/R-08-v7.2a)
51
  > - 🐙 **GitHub v7.2:** [Thomasseri/R-08-v7.2](https://github.com/Thomasseri/R-08-v7.2) | [SentinelHub/R-08-v7.2](https://huggingface.co/SentinelHub/R-08-v7.2)
52
 
53
  ---
54
 
55
  > ### 💼 Investment, Compute Grants & Enterprise Licensing (Investimenti & Partnership Industriali)
56
- > 🇬🇧 **Sentinel** is scaling the **Geodesic Pyramidal Sphere Architecture (`R-08`)** from 50M to **110M–1B+ parameters**, backed by **18 industrial patent filings** (Non-Euclidean Geodesic Mesh, Isomorphic `SSvvv` Lexical-Voice Engine, Post-Quantum Tamper-Resistant Keystone Security, Edge & Autonomous Systems).
57
  > **Open to Seed / Deep-Tech Investment, GPU/TPU Compute Grants, and Enterprise R&D Partnerships.**
58
  > 🇮🇹 **Aperto a Investimenti Seed / Deep-Tech, Grant Computazionali GPU/TPU e Licenze Industriali Enterprise.**
59
  > 📩 **Contact Founder & Inventor Thomas Seri:** 🌐 **[www.sentinelhub.eu](https://www.sentinelhub.eu)** | 🐙 **[GitHub (@Thomasseri)](https://github.com/Thomasseri)** | 🤗 **[Hugging Face (@SentinelHub)](https://huggingface.co/SentinelHub)** | 💬 **[Open an Inquiry / Issue](https://github.com/Thomasseri/R-08/issues/new)**
@@ -111,11 +111,10 @@ Per quantificare scientificamente il contributo dell'**architettura a 3 sfere ge
111
  | :--- | :---: | :---: | :---: | :---: | :---: | :---: |
112
  | **Parametri Totali** | **44.5M** | **51.6M** | **51.6M** | 44.9M | 44.9M | Pesi equivalenti (~45-51M) |
113
  | **Strati di Attenzione Densa** | **2 strati** | **2 strati** | **2 strati** | 14 strati | 14 strati | **-85.7% strati densi** |
114
- | **BLiMP-IT (Accuratezza Linguistica)** | **80.95%** | **82.02%** | **82.51%** 🏆 | 82.53% | 85.84% | Competitivo con soli 2 strati |
115
- | **MultiBLiMP (Ragionamento Sintattico)** | **73.46%** | **73.46%** | **76.71%** 🏆 | 85.15% | 86.83% | Nuovo picco geodetico (+3.25%) |
116
  | **Resistenza Allucinazioni (Zero-Shot)** | **62.5%** *(Fatti 4/4)* | **56.2%** *(Fatti 3/4)* | **56.2%** *(Fatti 4/4 100%)* | 56.2% | 68.8% | Zero allucinazioni su fatti reali |
117
  | **Throughput Training (MPS)** | **~10.150 tok/s** | **~7.200 tok/s** | **~7.694 tok/s** | ~8.650 tok/s | ~8.650 tok/s | Elevata efficienza computazionale |
118
- | **Consumo Energetico per Token** | **2.3 mJ / tok** | **2.4 mJ / tok** | **2.4 mJ / tok** | 3.3 mJ / tok | 3.3 mJ / tok | **-27.3% / -30.3% energia risparmiata** 🔋 |
119
  | **Impronta Memoria KV-Cache (512 ctx)** | **0.8 MB** | **0.8 MB** | **0.8 MB** | 5.6 MB | 5.6 MB | **-85.7% traffico RAM/Cache (7× più leggero)** 💾 |
120
 
121
  ---
@@ -129,30 +128,17 @@ Per quantificare scientificamente il contributo dell'**architettura a 3 sfere ge
129
  ![Curva di Efficienza RAM KV-Cache](./assets/kv_cache_ram_curve.svg)
130
 
131
  #### Insight Ingegneristico dello Studio A/B:
132
- 1. **Efficienza Energetica e Edge AI:** Il Transformer a 14 strati densi ottiene una precisione grammaticale superiore (85.8% vs 80.95% - 82.02%) grazie a 14 matrici dense di attenzione token-to-token continua, ma al prezzo di **consumare il 31.2% di energia in più** e richiedere **7 volte più memoria di KV-Cache** in RAM.
133
- 2. **La Forza della Geometria Geodetica:** Sentinel R08 raggiunge l'**80.95% - 82.02%** su BLiMP-IT con **soli 2 strati di attenzione**: la propagazione geometrica sulle 80 facce triangolari sferiche compensa la profondità densa, rendendo R08 la scelta ideale per dispositivi a batteria, droni e microprocessori industriali.
134
  3. **L'evoluzione di R-08 v7.2:** L'ipotesi sperimentale è stata validata con successo: con l'introduzione delle **Convoluzioni Causali 1D inter-sfera ($k=4$)** e dei **12 Poli Hub regionali dell'Icosaedro**, la generazione ha debellato i loop di balbuzie e l'accuratezza è balzata all'**82.02%** (vedi [R-08 v7.2](https://huggingface.co/SentinelHub/R-08-v7.2)).
135
 
136
  ---
137
 
138
- ### 2. Confronto Generale vs Modelli Foundation Open (~70M - 125M)
139
 
140
- Valutazione preliminare eseguita sui benchmark pubblici standard per la lingua italiana (`BLiMP-IT` 3.481 coppie e `MultiBLiMP` 2.916 coppie):
141
-
142
- | Modello | Creatore | Parametri | Strati Attenzione Densa | Peso Checkpoint | BLiMP-IT (Accuratezza Linguistica) | MultiBLiMP (Ragionamento Logico) |
143
- | :--- | :--- | :---: | :---: | :---: | :---: | :---: |
144
- | **OPT-125M** | Meta (Facebook AI) | 125M *(2.8× R08)* | 12 strati | ~500 MB | 62.5% | 58.0% |
145
- | **GPT-Neo-125M** | EleutherAI | 125M *(2.8× R08)* | 12 strati | ~500 MB | 68.2% | 61.5% |
146
- | **Pythia-70M** | EleutherAI | 70M *(1.6× R08)* | 6 strati | ~280 MB | 73.8% | 64.0% |
147
- | **GPT-2 Small** | OpenAI | 124M *(2.7× R08)* | 12 strati | ~498 MB | 74.5% - 80.1% | 66.8% |
148
- | [**Transformer Baseline (14 Layers)**](https://huggingface.co/SentinelHub/R-08-Transformer-Baseline) | Baseline A/B di Controllo | 44.9M | 14 strati | ~179 MB | 82.53% - 85.84% | 85.15% - 86.83% |
149
- | 🌐 **R08 (Rob / v7 Pyramid)** | **Thomas Seri / Sentinel** | **44.5M** | **2 strati** | **171.7 MB** | **80.95%** | **73.46%** |
150
- | 🚀 [**R08-v7.2 (Rob / 12 Hubs + 1D Conv)**](https://huggingface.co/SentinelHub/R-08-v7.2) | **Thomas Seri / Sentinel** | **51.6M** | **2 strati** | **206.4 MB** | **82.02%** 🏆 | **73.46%** |
151
 
152
  > [!NOTE]
153
- > **Trasparenza Metodologica & Invito a Riscontri Indipendenti:**
154
- > - **Dominio Linguistico Specializzato:** I test `BLiMP-IT` e `MultiBLiMP` misurano specificamente la sensibilità grammaticale in lingua italiana, dominio primario per cui R08 è stato curato e addestrato. Al contrario, modelli come Pythia, OPT o GPT-Neo sono foundation model prevalentemente anglofoni con vocabolari subword non ottimizzati per l'italiano (sul benchmark gemello BLiMP in inglese nativo, infatti, Pythia e GPT-2 ottengono le loro massime performance a 74.6% e 85.0%, mentre R08 non essendo addestrato su inglese si attesta sul 52.8%).
155
- > - **Primi Riscontri di Efficienza Architetturale:** L'architettura geodetica a 3 sfere con soli 2 strati di attenzione densa consente una notevole efficienza di calcolo (fino a -85.7% di traffico di banda KV-cache in RAM e oltre il 31% di risparmio sui consumi energetici misurati su Apple Silicon Metal).
156
  > - **Aperto alla Community:** Trattandosi di un'architettura sperimentale non convenzionale, questi numeri di laboratorio attendono ulteriori riscontri e repliche indipendenti. Il codice e i pesi sono 100% open source proprio per invitare ricercatori, ingegneri e appassionati a testare il modello nei loro ambienti e a condividere feedback, limiti o suggerimenti.
157
 
158
  ---
@@ -204,5 +190,5 @@ Il modello **R08** e il sistema **Sentinel LifeOS** sono distribuiti sotto la li
204
  | **Parametri Totali in `model.safetensors`** | **`44.936.996`** tensori totali salvati (composti da **`44.575.667` parametri neurali attivi** + **`361.329` buffer topologici** e indici di adiacenza dell'icosaedro geodetico) |
205
  | **Runtime di Esecuzione** | **Motore Geometrico Nativo `modeling_geodesic.py`** (`trust_remote_code=True`). Essendo una topologia sferico-piramidale non-Transformer, utilizza obbligatoriamente il proprio runtime nativo invece di wrapper nati solo per Transformer piatti. |
206
  | **Finestra di Contesto Operativa** | **`256 – 512` token nativi** (valutata fino a **`2.048` token** in inferenza con soli `0,8 MB – 3,2 MB` di KV-Cache). |
207
- | **Benchmark & Confronto Internazionale** | **`80,95%` su BLiMP-IT** (3.481 coppie), **`73,46%` su MultiBLiMP Italiano** (2.916 coppie) e **`62,5%` (10/16) su TruthfulQA Anti-Allucinazione Zero-Shot**, superando **`Pythia-70M`** (`50,0%`) e **`GPT-2 Base 124M`** (`50,0%`) con un footprint RAM di soli **`~120 MB`** e **`2,30 mJ/token`**. |
208
  | **Lingua & Licenza** | Italiano (`it`) · **R08 Non-Commercial Research & Ethical License v2.0** (Foro Esclusivo di Milano) |
 
38
  **Organizzazione:** SentinelHub
39
  **Hardware di Sviluppo:** Apple Silicon (Metal Performance Shaders - MPS)
40
  **Versione Successiva Evoluta:** 🚀 [**Sentinel R-08 v7.2 (51.6M)**](https://huggingface.co/SentinelHub/R-08-v7.2) | [GitHub v7.2](https://github.com/Thomasseri/R-08-v7.2)
41
+ **Nuovo modello:** 🌟 [**Sentinel R-08 v7.2a DA ZERO (51.6M)**](https://github.com/Thomasseri/R-08-v7.2a) | [Hugging Face v7.2a](https://huggingface.co/SentinelHub/R-08-v7.2a)
42
 
43
  > [!TIP]
44
+ > ### 🚀 È Rilasciato il Nuovo Modello Sentinel R08-v7.2 (51.6M) & v7.2a DA ZERO
45
  > L'evoluzione neurale geometrica di Sentinel LifeOS è ora disponibile con **Convoluzioni Causali 1D Inter-Stadio ($k=4$)** e **12 Hub Regionali dell'Icosaedro**:
46
+ > - **BLiMP-IT: 82.02% (v7.2) e 82.51% (v7.2a DA ZERO), il migliore tra gli R-08** (Transformer di controllo a 14 strati: 85.84%)
47
+ > - **MultiBLiMP: 76.71% (v7.2a)** (Transformer di controllo: 86.83%)
48
  > - **Eliminazione totale di loop e balbuzie ciclica** nei testi lunghi
49
+ > - **-85.7% di memoria KV-Cache** rispetto al Transformer di controllo (2 strati di attenzione contro 14)
50
  > - 🌟 **GitHub v7.2a:** [Thomasseri/R-08-v7.2a](https://github.com/Thomasseri/R-08-v7.2a) | [SentinelHub/R-08-v7.2a](https://huggingface.co/SentinelHub/R-08-v7.2a)
51
  > - 🐙 **GitHub v7.2:** [Thomasseri/R-08-v7.2](https://github.com/Thomasseri/R-08-v7.2) | [SentinelHub/R-08-v7.2](https://huggingface.co/SentinelHub/R-08-v7.2)
52
 
53
  ---
54
 
55
  > ### 💼 Investment, Compute Grants & Enterprise Licensing (Investimenti & Partnership Industriali)
56
+ > 🇬🇧 **Sentinel** is scaling the **Geodesic Pyramidal Sphere Architecture (`R-08`)** from 50M to **110M–1B+ parameters**, with **patent applications filed at the Italian Patent and Trademark Office (UIBM)**.
57
  > **Open to Seed / Deep-Tech Investment, GPU/TPU Compute Grants, and Enterprise R&D Partnerships.**
58
  > 🇮🇹 **Aperto a Investimenti Seed / Deep-Tech, Grant Computazionali GPU/TPU e Licenze Industriali Enterprise.**
59
  > 📩 **Contact Founder & Inventor Thomas Seri:** 🌐 **[www.sentinelhub.eu](https://www.sentinelhub.eu)** | 🐙 **[GitHub (@Thomasseri)](https://github.com/Thomasseri)** | 🤗 **[Hugging Face (@SentinelHub)](https://huggingface.co/SentinelHub)** | 💬 **[Open an Inquiry / Issue](https://github.com/Thomasseri/R-08/issues/new)**
 
111
  | :--- | :---: | :---: | :---: | :---: | :---: | :---: |
112
  | **Parametri Totali** | **44.5M** | **51.6M** | **51.6M** | 44.9M | 44.9M | Pesi equivalenti (~45-51M) |
113
  | **Strati di Attenzione Densa** | **2 strati** | **2 strati** | **2 strati** | 14 strati | 14 strati | **-85.7% strati densi** |
114
+ | **BLiMP-IT (Accuratezza Linguistica)** | **80.95%** | **82.02%** | **82.51%** | 82.53% | 85.84% | Competitivo con soli 2 strati |
115
+ | **MultiBLiMP (Ragionamento Sintattico)** | **73.46%** | **73.46%** | **76.71%** | 85.15% | 86.83% | Nuovo picco geodetico (+3.25%) |
116
  | **Resistenza Allucinazioni (Zero-Shot)** | **62.5%** *(Fatti 4/4)* | **56.2%** *(Fatti 3/4)* | **56.2%** *(Fatti 4/4 100%)* | 56.2% | 68.8% | Zero allucinazioni su fatti reali |
117
  | **Throughput Training (MPS)** | **~10.150 tok/s** | **~7.200 tok/s** | **~7.694 tok/s** | ~8.650 tok/s | ~8.650 tok/s | Elevata efficienza computazionale |
 
118
  | **Impronta Memoria KV-Cache (512 ctx)** | **0.8 MB** | **0.8 MB** | **0.8 MB** | 5.6 MB | 5.6 MB | **-85.7% traffico RAM/Cache (7× più leggero)** 💾 |
119
 
120
  ---
 
128
  ![Curva di Efficienza RAM KV-Cache](./assets/kv_cache_ram_curve.svg)
129
 
130
  #### Insight Ingegneristico dello Studio A/B:
131
+ 1. **Compromesso qualità / memoria:** Il Transformer a 14 strati densi ottiene una precisione grammaticale superiore (85.84% contro 80.95% – 82.51% su BLiMP-IT, 86.83% contro 73.46% – 76.71% su MultiBLiMP), ma richiede **7 volte più memoria di KV-Cache** in RAM (5.6 MB contro 0.8 MB a 512 token).
132
+ 2. **Geometria Geodetica:** Sentinel R08 raggiunge l'**80.95% – 82.51%** su BLiMP-IT con **soli 2 strati di attenzione**, a circa 3–5 punti dal Transformer a 14 strati: un compromesso interessante per dispositivi con poca memoria.
133
  3. **L'evoluzione di R-08 v7.2:** L'ipotesi sperimentale è stata validata con successo: con l'introduzione delle **Convoluzioni Causali 1D inter-sfera ($k=4$)** e dei **12 Poli Hub regionali dell'Icosaedro**, la generazione ha debellato i loop di balbuzie e l'accuratezza è balzata all'**82.02%** (vedi [R-08 v7.2](https://huggingface.co/SentinelHub/R-08-v7.2)).
134
 
135
  ---
136
 
137
+ ### 2. Confronto con modelli esterni
138
 
139
+ Un confronto con modelli open esterni (Pythia-70M, GPT-2, OPT, GPT-Neo) sugli stessi benchmark italiani non è ancora stato eseguito con uno script riproducibile. Le versioni precedenti di questa pagina riportavano numeri per questi modelli che non erano verificati: sono stati rimossi il 6/11/2026 e verranno ripubblicati solo dopo una misura reale.
 
 
 
 
 
 
 
 
 
 
140
 
141
  > [!NOTE]
 
 
 
142
  > - **Aperto alla Community:** Trattandosi di un'architettura sperimentale non convenzionale, questi numeri di laboratorio attendono ulteriori riscontri e repliche indipendenti. Il codice e i pesi sono 100% open source proprio per invitare ricercatori, ingegneri e appassionati a testare il modello nei loro ambienti e a condividere feedback, limiti o suggerimenti.
143
 
144
  ---
 
190
  | **Parametri Totali in `model.safetensors`** | **`44.936.996`** tensori totali salvati (composti da **`44.575.667` parametri neurali attivi** + **`361.329` buffer topologici** e indici di adiacenza dell'icosaedro geodetico) |
191
  | **Runtime di Esecuzione** | **Motore Geometrico Nativo `modeling_geodesic.py`** (`trust_remote_code=True`). Essendo una topologia sferico-piramidale non-Transformer, utilizza obbligatoriamente il proprio runtime nativo invece di wrapper nati solo per Transformer piatti. |
192
  | **Finestra di Contesto Operativa** | **`256 – 512` token nativi** (valutata fino a **`2.048` token** in inferenza con soli `0,8 MB – 3,2 MB` di KV-Cache). |
193
+ | **Benchmark** | **`80,95%` su BLiMP-IT** (3.481 coppie) e **`73,46%` su MultiBLiMP Italiano** (2.916 coppie). TruthfulQA ridotto: `62,5%` (10/16 domande, campione troppo piccolo per confronti). Footprint RAM **`~120 MB`**. |
194
  | **Lingua & Licenza** | Italiano (`it`) · **R08 Non-Commercial Research & Ethical License v2.0** (Foro Esclusivo di Milano) |
assets/r08_logo.jpg DELETED
Binary file (36.9 kB)
 
config.json DELETED
@@ -1,51 +0,0 @@
1
- {
2
- "model_name": "R08",
3
- "architectures": [
4
- "R08Model"
5
- ],
6
- "model_type": "geodesic_mesh_pyramid",
7
- "architecture_name": "cascade_stages_attn_mesh",
8
- "wiring": "serial_emb_mesh_x3_head_pezzi",
9
- "d_model": 256,
10
- "max_seq_len": 512,
11
- "seq_len": 256,
12
- "vocab_pezzi": 1251,
13
- "vocab_parole": 17908,
14
- "vocab_unioni": 20006,
15
- "n_attn_layers": 2,
16
- "n_heads": 8,
17
- "pyramid_center": true,
18
- "pyramid_config": {
19
- "d_stretto": 128,
20
- "d_centro": 512,
21
- "stadi": 3,
22
- "facce": 80,
23
- "vertici": 42,
24
- "parametri_piramide": 753408
25
- },
26
- "mesh_topology": {
27
- "geometry": "geodesic_sphere_icosahedron",
28
- "frequency": 2,
29
- "total_faces": 80,
30
- "total_vertices": 42,
31
- "neighbors_per_face": 3
32
- },
33
- "routing": {
34
- "hash_routing": true,
35
- "router_v6": true,
36
- "no_walk": true,
37
- "compass_spiral": true
38
- },
39
- "total_parameters": 44575907,
40
- "trainable_parameters": 44575907,
41
- "auto_map": {
42
- "AutoConfig": "modeling_geodesic.GeodesicConfig",
43
- "AutoModelForCausalLM": "modeling_geodesic.GeodesicPyramidModel"
44
- },
45
- "torch_dtype": "float32",
46
- "transformers_version": "4.45.0",
47
- "author": "Thomas Seri (Sentinel)",
48
- "license": "R08-Non-Commercial-Ethical-License-v2.0",
49
- "jurisdiction": "Court of Milan (Sezione Specializzata in Materia di Impresa), Italy",
50
- "prohibited_uses": "Commercial, Military, Defense, Clean-Room Reverse Engineering, Model Distillation"
51
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fast_walk.py DELETED
@@ -1,401 +0,0 @@
1
- """
2
- Cammino vettorizzato sulla mesh per CascadeMeshModel / V1OnlyMeshModel.
3
-
4
- Sostituisce il loop Python per faccia di ``CascadeMeshModel.walk_token``
5
- (``for f_id in face[active].unique().tolist()`` + ``forward_face`` con
6
- ``.item()`` e maschere booleane) con un dispatch raggruppato alla MegaBlocks:
7
-
8
- - i parametri dei TriangleBlock sono impilati per face id (stack
9
- differenziabile dei parametri esistenti: state_dict e checkpoint invariati);
10
- - i token attivi sono disposti per faccia (posizioni da un cumsum, niente
11
- sort): le facce con pochi token in un elemento di batch per faccia, quelle
12
- affollate in blocchi da ``block_rows`` righe con i pesi raccolti per blocco
13
- (vedi ``grouped_block_forward`` per il perché dei percorsi);
14
- - ogni Linear del blocco diventa un ``baddbmm`` per percorso;
15
- - le uscite tornano ai token con ``index_select`` e ``torch.where``.
16
-
17
- Le forme dipendono solo dal numero di token B, mai dai dati: nel salto non ci
18
- sono ``unique``, ``tolist``, ``nonzero`` né ``.item()``.
19
-
20
- Puro refactoring: proiezione sulla faccia, router, Gumbel-ST, soglia di uscita
21
- e peso straight-through sull'uscita intera del blocco sono quelli del modello
22
- (si chiamano gli stessi metodi ``project_to_face``, ``_route_step``,
23
- ``_scatter_side_oh`` nello stesso ordine), quindi anche il consumo del
24
- generatore casuale è lo stesso. Restano due sincronizzazioni, entrambe
25
- necessarie per l'equivalenza:
26
- - ``active.any()`` a inizio salto con routing soft: il codice originale esce
27
- dal ciclo quando tutti i token sono usciti e da lì non estrae più rumore
28
- Gumbel (in routing hard i salti in più sono no-op e il controllo è saltato);
29
- - nel backward, una lettura per banca della maschera delle facce visitate:
30
- le facce mai visitate devono restare con grad None come nel loop.
31
-
32
- ``fast_walk_token_v6`` è il cammino del router v6 (vedi geodesic_stages_model):
33
- stesso dispatch raggruppato, peso straight-through passato come ``gate`` sul
34
- solo ramo dell'esperto.
35
- """
36
- from __future__ import annotations
37
-
38
- from typing import Dict, List, Optional, Sequence, Tuple
39
-
40
- import torch
41
- import torch.nn as nn
42
- import torch.nn.functional as F
43
-
44
- DEFAULT_BLOCK_ROWS = 16 # >= 16: regime "M grande" del Linear su MPS
45
- DEFAULT_DENSE_ROWS = 15 # <= 15: regime "M piccolo" del Linear su MPS
46
-
47
- _NAMES = ("ln_w", "ln_b", "w1", "b1", "w2", "b2", "wr", "br")
48
-
49
-
50
- def _check_block(blk: nn.Module) -> None:
51
- norm, ff, router = blk.norm, blk.ff, blk.router
52
- ok = (
53
- isinstance(norm, nn.LayerNorm)
54
- and norm.weight is not None
55
- and norm.bias is not None
56
- and len(norm.normalized_shape) == 1
57
- and isinstance(ff, nn.Sequential)
58
- and len(ff) == 3
59
- and isinstance(ff[0], nn.Linear)
60
- and isinstance(ff[1], nn.GELU)
61
- and ff[1].approximate == "none"
62
- and isinstance(ff[2], nn.Linear)
63
- and ff[0].bias is not None
64
- and ff[2].bias is not None
65
- and isinstance(router, nn.Linear)
66
- and router.bias is not None
67
- )
68
- if not ok:
69
- raise TypeError("fast_walk: TriangleBlock con struttura inattesa (LN → Linear → GELU → Linear, router)")
70
-
71
-
72
- def _face_blocks(bank: nn.Module) -> List[nn.Module]:
73
- """TriangleBlock in ordine di face id (il ModuleList è in ordine di packing a coppie)."""
74
- slots = bank.__dict__.get("_fast_walk_slots")
75
- if slots is None:
76
- slots = [int(s) for s in bank.face_to_slot.detach().cpu().tolist()]
77
- if sorted(slots) != list(range(len(bank.blocks))) or len(slots) != int(bank.num_faces):
78
- raise ValueError("fast_walk: face_to_slot non è una permutazione dei blocchi")
79
- for s in slots:
80
- _check_block(bank.blocks[s])
81
- bank.__dict__["_fast_walk_slots"] = slots
82
- return [bank.blocks[s] for s in slots]
83
-
84
-
85
- def _block_params(blk: nn.Module) -> Tuple[torch.Tensor, ...]:
86
- return (
87
- blk.norm.weight,
88
- blk.norm.bias,
89
- blk.ff[0].weight,
90
- blk.ff[0].bias,
91
- blk.ff[2].weight,
92
- blk.ff[2].bias,
93
- blk.router.weight,
94
- blk.router.bias,
95
- )
96
-
97
-
98
- class _VisitedFaces:
99
- """Facce che hanno eseguito il blocco in almeno un salto (maschera su device, letta nel backward)."""
100
-
101
- def __init__(self) -> None:
102
- self.mask: Optional[torch.Tensor] = None
103
- self._host: Optional[List[bool]] = None
104
-
105
- def add(self, counts: torch.Tensor) -> None:
106
- hit = counts > 0
107
- self.mask = hit if self.mask is None else (self.mask | hit)
108
-
109
- def host(self, n_faces: int) -> List[bool]:
110
- if self._host is None:
111
- self._host = [False] * n_faces if self.mask is None else [bool(v) for v in self.mask.tolist()]
112
- return self._host
113
-
114
-
115
- class _StackFaces(torch.autograd.Function):
116
- """torch.stack per tipo di parametro; nel backward le facce mai visitate ricevono None.
117
-
118
- Nel loop originale i parametri di una faccia non visitata non entrano nel
119
- grafo e restano con grad None (AdamW li salta: niente momento né weight
120
- decay); un grad di zeri cambierebbe l'aggiornamento. Per lo stesso motivo
121
- un tipo di parametro fuori dal grafo differenziabile (il router con routing
122
- hard) deve restare None: niente gradienti materializzati a zero.
123
- """
124
-
125
- @staticmethod
126
- def forward(ctx, visited: _VisitedFaces, n_faces: int, *flat: torch.Tensor):
127
- ctx.set_materialize_grads(False)
128
- ctx.visited = visited
129
- ctx.n_faces = n_faces
130
- return tuple(torch.stack(flat[k : k + n_faces]) for k in range(0, len(flat), n_faces))
131
-
132
- @staticmethod
133
- def backward(ctx, *grads):
134
- hit = ctx.visited.host(ctx.n_faces)
135
- out: List[Optional[torch.Tensor]] = [None, None]
136
- for g in grads:
137
- if g is None:
138
- out.extend([None] * ctx.n_faces)
139
- else:
140
- out.extend(gf if h else None for gf, h in zip(g.unbind(0), hit))
141
- return tuple(out)
142
-
143
-
144
- def stack_bank(bank: nn.Module) -> Dict[str, object]:
145
- """Parametri della banca impilati per face id: ln (F,d), w1 (F,h,d), w2 (F,d,h), router (F,4,d).
146
-
147
- Con autograd attivo lo stack è nel grafo (i gradienti tornano ai parametri
148
- per faccia) e ``st["visited"]`` va aggiornato dal cammino. Senza gradienti
149
- il risultato resta in cache finché i parametri non cambiano (data_ptr e
150
- contatore di versione).
151
- """
152
- blocks = _face_blocks(bank)
153
- params = [_block_params(b) for b in blocks]
154
- n_faces = len(blocks)
155
- grad_mode = torch.is_grad_enabled() and any(p.requires_grad for ps in params for p in ps)
156
- if grad_mode:
157
- visited = _VisitedFaces()
158
- flat = [ps[i] for i in range(len(_NAMES)) for ps in params]
159
- st: Dict[str, object] = dict(zip(_NAMES, _StackFaces.apply(visited, n_faces, *flat)))
160
- st["visited"] = visited
161
- bank.__dict__.pop("_fast_walk_cache", None)
162
- else:
163
- key = (torch.is_inference_mode_enabled(),) + tuple(
164
- (p.data_ptr(), p._version, p.device, p.dtype) for ps in params for p in ps
165
- )
166
- cached = bank.__dict__.get("_fast_walk_cache")
167
- if cached is not None and cached[0] == key:
168
- return cached[1]
169
- st = {n: torch.stack([ps[i] for ps in params]) for i, n in enumerate(_NAMES)}
170
- st["visited"] = None
171
- bank.__dict__["_fast_walk_cache"] = (key, st)
172
- st["eps"] = float(blocks[0].norm.eps)
173
- st["num_faces"] = n_faces
174
- return st
175
-
176
-
177
- def _triangle(
178
- xb: torch.Tensor, w: Sequence[torch.Tensor], eps: float, gate: Optional[torch.Tensor] = None
179
- ) -> Tuple[torch.Tensor, torch.Tensor]:
180
- """TriangleBlock.forward a gruppi: xb (G, R, d), pesi (G, ...) di un blocco per gruppo.
181
-
182
- gate (G, R, 1), router v6: xb + gate · ff(norm(xb)), il peso non tocca il residuo.
183
- """
184
- ln_w, ln_b, w1, b1, w2, b2, wr, br = w
185
- d = xb.shape[-1]
186
- xn = F.layer_norm(xb, (d,), None, None, eps) * ln_w.unsqueeze(1) + ln_b.unsqueeze(1)
187
- hid = F.gelu(torch.baddbmm(b1.unsqueeze(1), xn, w1.transpose(1, 2)))
188
- delta = torch.baddbmm(b2.unsqueeze(1), hid, w2.transpose(1, 2))
189
- xo = xb + delta if gate is None else xb + gate * delta
190
- return xo, torch.baddbmm(br.unsqueeze(1), xo, wr.transpose(1, 2))
191
-
192
-
193
- def _scatter_rows(x: torch.Tensor, dest: torch.Tensor, rows: int) -> torch.Tensor:
194
- """Buffer (rows, d) con x[i] alla riga dest[i]; dest == rows è la riga di scarto."""
195
- return x.new_zeros(rows + 1, x.shape[1]).index_copy(0, dest, x)[:rows]
196
-
197
-
198
- def grouped_block_forward(
199
- st: Dict[str, object],
200
- x: torch.Tensor,
201
- face: torch.Tensor,
202
- active: torch.Tensor,
203
- block_rows: int = DEFAULT_BLOCK_ROWS,
204
- dense_rows: int = DEFAULT_DENSE_ROWS,
205
- gate: Optional[torch.Tensor] = None,
206
- ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
207
- """TriangleBlock della propria faccia su ogni token attivo, senza sincronizzazioni.
208
-
209
- x (B, d), face (B,) long, active (B,) bool, gate (B, 1) opzionale (router v6:
210
- peso straight-through sul solo ramo dell'esperto, disposto come x).
211
- Ritorna (x_out, logits, counts): (B, d), (B, 4) e token attivi per faccia (F,).
212
- Sulle righe inattive x_out/logits sono finiti ma arbitrari (il chiamante li
213
- scarta con torch.where).
214
-
215
- Nel loop originale ogni faccia fa i suoi Linear su tutte e sole le sue n_f
216
- righe, e il kernel dipende da n_f: su MPS uno per n_f <= 15 e uno per
217
- n_f >= 16, su CPU (BLAS) uno per n_f == 1 e uno per n_f >= 2; dentro lo
218
- stesso regime le righe sono bit-identiche qualunque sia n_f. Per restare
219
- bit-identici ci sono tre percorsi a forma fissa, ognuno nel regime giusto:
220
- - facce con n_f <= dense_rows (da 2 su CPU): un elemento di batch per
221
- faccia con dense_rows righe, sui pesi impilati senza gather;
222
- - su CPU, facce con un solo token: un elemento da 1 riga per faccia;
223
- - facce con n_f > dense_rows: blocchi da block_rows righe (padding solo
224
- nell'ultimo blocco di ogni faccia) con i pesi raccolti per blocco.
225
- """
226
- b, d = x.shape
227
- n_faces = int(st["num_faces"])
228
- s, c = int(block_rows), int(dense_rows)
229
- weights = [st[n] for n in _NAMES] # (F, ...)
230
- eps = float(st["eps"]) # type: ignore[arg-type]
231
- dev = x.device
232
- single = dev.type == "cpu"
233
- lo = 2 if single else 1
234
-
235
- key = torch.where(active, face, torch.full_like(face, n_faces)) # inattivi → classe fittizia F
236
- face_c = key.clamp(max=n_faces - 1)
237
- onehot = (key.unsqueeze(1) == torch.arange(n_faces + 1, device=dev)).to(torch.int64)
238
- rank = onehot.cumsum(0).gather(1, key.unsqueeze(1)).squeeze(1) - 1 # posizione nella faccia
239
- counts = onehot[:, :n_faces].sum(0)
240
- n_tok = counts.gather(0, face_c)
241
- in_big = active & (n_tok > c)
242
- in_dense = active & (n_tok >= lo) & ~in_big
243
- zero = torch.zeros_like(rank)
244
- parts: List[Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]] = [] # (maschera, x_out, logit, riga)
245
-
246
- def _gate(dest: torch.Tensor, rows: int, shape: Tuple[int, int]) -> Optional[torch.Tensor]:
247
- return None if gate is None else _scatter_rows(gate, dest, rows).view(*shape, 1)
248
-
249
- if c >= lo:
250
- dest = torch.where(in_dense, face * c + rank, torch.full_like(rank, n_faces * c))
251
- xo, lg = _triangle(
252
- _scatter_rows(x, dest, n_faces * c).view(n_faces, c, d), weights, eps, _gate(dest, n_faces * c, (n_faces, c))
253
- )
254
- parts.append((in_dense, xo.reshape(-1, d), lg.reshape(n_faces * c, -1), torch.where(in_dense, dest, zero)))
255
- if single:
256
- alone = active & (n_tok == 1)
257
- dest = torch.where(alone, face, torch.full_like(face, n_faces))
258
- xo, lg = _triangle(
259
- _scatter_rows(x, dest, n_faces).unsqueeze(1), weights, eps, _gate(dest, n_faces, (n_faces, 1))
260
- )
261
- parts.append((alone, xo.reshape(-1, d), lg.reshape(n_faces, -1), face_c))
262
-
263
- # Limite statico: sum_f ceil(n_f / s) <= ceil(B / s) + facce con n_f > c.
264
- n_blocks = -(-b // s) + min(n_faces, b // (c + 1))
265
- rows = n_blocks * s
266
- nblk = torch.div(torch.where(counts > c, counts, 0) + (s - 1), s, rounding_mode="floor")
267
- blk_end = nblk.cumsum(0)
268
- blk_start = blk_end - nblk
269
- dest = torch.where(in_big, blk_start.gather(0, face_c) * s + rank, torch.full_like(rank, rows))
270
- blk_face = (blk_end.unsqueeze(0) <= torch.arange(n_blocks, device=dev).unsqueeze(1)).sum(1)
271
- blk_face = blk_face.clamp(max=n_faces - 1) # blocchi oltre il totale: righe a zero, uscite ignorate
272
- gathered = [w.index_select(0, blk_face) for w in weights]
273
- xo, lg = _triangle(_scatter_rows(x, dest, rows).view(n_blocks, s, d), gathered, eps, _gate(dest, rows, (n_blocks, s)))
274
- row = torch.where(in_big, dest, zero)
275
- out = xo.reshape(rows, d).index_select(0, row)
276
- logits = lg.reshape(rows, -1).index_select(0, row)
277
-
278
- for mask, p_out, p_lg, p_row in parts:
279
- m = mask.unsqueeze(-1)
280
- out = torch.where(m, p_out.index_select(0, p_row), out)
281
- logits = torch.where(m, p_lg.index_select(0, p_row), logits)
282
- return out, logits, counts
283
-
284
-
285
- def _build_paths(face0: torch.Tensor, codes: Sequence[torch.Tensor]) -> List[List[int]]:
286
- start = face0.tolist()
287
- if not codes:
288
- return [[int(f)] for f in start]
289
- table = torch.stack(list(codes), dim=1).tolist()
290
- return [[int(f)] + [c for c in row if c != -2] for f, row in zip(start, table)]
291
-
292
-
293
- def fast_walk_token(
294
- model: nn.Module,
295
- h: torch.Tensor,
296
- bank: nn.Module,
297
- to_sphere: nn.Linear,
298
- record_path: bool = False,
299
- block_rows: Optional[int] = None,
300
- dense_rows: Optional[int] = None,
301
- ) -> Tuple[torch.Tensor, Optional[List[List[int]]]]:
302
- """Stessa semantica di CascadeMeshModel.walk_token (loop per faccia), vettorizzata."""
303
- b = h.shape[0]
304
- face, face_oh = model.project_to_face(h, to_sphere)
305
- face0 = face
306
- max_hops = int(model.walk.max_hops)
307
- if b == 0 or max_hops <= 0:
308
- return h, (_build_paths(face0, []) if record_path else None)
309
-
310
- st = stack_bank(bank)
311
- rows = int(block_rows or getattr(model, "fast_walk_block_rows", 0) or DEFAULT_BLOCK_ROWS)
312
- dense = dense_rows if dense_rows is not None else getattr(model, "fast_walk_dense_rows", None)
313
- dense = DEFAULT_DENSE_ROWS if dense is None else int(dense)
314
- soft = not model._hard_routing()
315
- active = torch.ones(b, dtype=torch.bool, device=h.device)
316
- codes: List[torch.Tensor] = []
317
-
318
- for hop in range(max_hops):
319
- if hop > 0 and soft and not bool(active.any()):
320
- break
321
- out, lg, counts = grouped_block_forward(st, h, face, active, rows, dense)
322
- if st["visited"] is not None:
323
- st["visited"].add(counts) # type: ignore[union-attr]
324
- act = active.unsqueeze(-1)
325
- if face_oh is not None:
326
- # ST: w≈1 in forward; backward → face_oh → to_sphere / router
327
- w = face_oh.gather(1, face.unsqueeze(1))
328
- h = torch.where(act, out * w, h)
329
- else:
330
- h = torch.where(act, out, h)
331
- logits = torch.where(act, lg, torch.zeros_like(lg))
332
- next_face, exit_mask, side_oh = model._route_step(face, logits)
333
- moved = active & ~exit_mask
334
- if side_oh is not None:
335
- scale = side_oh.sum(dim=-1, keepdim=True)
336
- h = torch.where(act, h * scale, h)
337
- routed_oh = model._scatter_side_oh(face, side_oh)
338
- if face_oh is None:
339
- face_oh = routed_oh
340
- else:
341
- face_oh = torch.where(moved.unsqueeze(-1), routed_oh, face_oh)
342
- face = torch.where(moved, next_face, face)
343
- just_exit = active & exit_mask
344
- active = active & ~exit_mask
345
- if record_path:
346
- codes.append(torch.where(moved, face, torch.where(just_exit, -1, -2)))
347
-
348
- return h, (_build_paths(face0, codes) if record_path else None)
349
-
350
-
351
- def fast_walk_token_v6(
352
- model: nn.Module,
353
- h: torch.Tensor,
354
- bank: nn.Module,
355
- to_sphere: Optional[nn.Linear],
356
- entry_ids: Optional[torch.Tensor] = None,
357
- record_path: bool = False,
358
- block_rows: Optional[int] = None,
359
- dense_rows: Optional[int] = None,
360
- active: Optional[torch.Tensor] = None,
361
- ) -> Tuple[torch.Tensor, Optional[List[List[int]]]]:
362
- """Cammino del router v6 / hash, vettorizzato: semantica di ``CascadeMeshModel._walk_token_v6_loop``.
363
-
364
- Nessuna sincronizzazione nel salto: senza l'uscita anticipata su
365
- ``active.any()`` i salti dopo che tutti i token sono usciti sono no-op.
366
- ``active`` opzionale (B,): token inattivi (pad/unk stadi 2/3) saltano le FFN;
367
- in ``record_path`` la faccia d'ingresso è ``-1``.
368
- """
369
- b = h.shape[0]
370
- face, w = model._v6_entry(h, bank, to_sphere, entry_ids)
371
- max_hops = int(model.walk.max_hops)
372
- if active is None:
373
- active = torch.ones(b, dtype=torch.bool, device=h.device)
374
- else:
375
- active = active.to(device=h.device, dtype=torch.bool)
376
- face0 = torch.where(active, face, torch.full_like(face, -1))
377
- if b == 0 or max_hops <= 0:
378
- return h, (_build_paths(face0, []) if record_path else None)
379
-
380
- st = stack_bank(bank)
381
- rows = int(block_rows or getattr(model, "fast_walk_block_rows", 0) or DEFAULT_BLOCK_ROWS)
382
- dense = dense_rows if dense_rows is not None else getattr(model, "fast_walk_dense_rows", None)
383
- dense = DEFAULT_DENSE_ROWS if dense is None else int(dense)
384
- codes: List[torch.Tensor] = []
385
-
386
- for _ in range(max_hops):
387
- out, lg, counts = grouped_block_forward(st, h, face, active, rows, dense, gate=w)
388
- if st["visited"] is not None:
389
- st["visited"].add(counts) # type: ignore[union-attr]
390
- act = active.unsqueeze(-1)
391
- h = torch.where(act, out, h)
392
- logits = torch.where(act, lg, torch.zeros_like(lg))
393
- next_face, exit_mask, w = model._v6_route(face, logits)
394
- moved = active & ~exit_mask
395
- face = torch.where(moved, next_face, face)
396
- just_exit = active & exit_mask
397
- active = active & ~exit_mask
398
- if record_path:
399
- codes.append(torch.where(moved, face, torch.where(just_exit, -1, -2)))
400
-
401
- return h, (_build_paths(face0, codes) if record_path else None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
geodesic_network.py DELETED
@@ -1,355 +0,0 @@
1
- """
2
- Rete neurale su mesh geodetica sferica (PyTorch).
3
-
4
- - TriangleBlock: pesi locali + router ternario (3 lati)
5
- - CausalAttnBlock: self-attention causale + FF (contesto temporale)
6
- - SphericalMeshModel: emb → CausalAttn × N → walk mesh batched → LM head
7
- - Pesi faccia impacchettati a coppie (pair packing) per buffer contigui GEMM-friendly
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- from dataclasses import dataclass
13
- from typing import List, Optional, Tuple
14
-
15
- import math
16
- import numpy as np
17
- import torch
18
- import torch.nn as nn
19
- import torch.nn.functional as F
20
-
21
- from mesh_topology import GeodesicMesh, build_geodesic_mesh, nearest_face
22
-
23
-
24
- @dataclass
25
- class WalkConfig:
26
- max_hops: int = 8
27
- # Se la probabilità del “exit” (soft) supera la soglia, si ferma prima
28
- exit_threshold: float = 0.55
29
- # Temperatura del router ternario
30
- router_temperature: float = 1.0
31
- # Se True, usa argmax (discreto); altrimenti Gumbel-Softmax straight-through
32
- hard_routing: bool = True
33
-
34
-
35
- class TriangleBlock(nn.Module):
36
- """Micro-blocco su una faccia: MLP locale + logits verso 3 lati + exit."""
37
-
38
- def __init__(self, d_model: int, d_hidden: Optional[int] = None):
39
- super().__init__()
40
- h = d_hidden or d_model
41
- self.norm = nn.LayerNorm(d_model)
42
- self.ff = nn.Sequential(
43
- nn.Linear(d_model, h),
44
- nn.GELU(),
45
- nn.Linear(h, d_model),
46
- )
47
- # 3 lati + 1 “esci / emetti”
48
- self.router = nn.Linear(d_model, 4)
49
-
50
- def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
51
- """
52
- x: (..., d)
53
- returns: (x_out, router_logits) con logits shape (..., 4)
54
- """
55
- h = self.ff(self.norm(x))
56
- x_out = x + h
57
- logits = self.router(x_out)
58
- return x_out, logits
59
-
60
-
61
- class PairedFaceBank(nn.Module):
62
- """
63
- Banca pesi: per ogni coppia di facce adiacenti memorizza 2 TriangleBlock
64
- in un modulo ModuleList, con indici face→block per lookup O(1).
65
- I parametri di ogni coppia restano contigui nel ModuleList (ordine packing).
66
- """
67
-
68
- def __init__(self, mesh: GeodesicMesh, d_model: int, d_hidden: Optional[int] = None):
69
- super().__init__()
70
- self.num_faces = mesh.num_faces
71
- self.register_buffer("adjacency", torch.from_numpy(mesh.adjacency.astype(np.int64)))
72
- self.register_buffer("pairs", torch.from_numpy(mesh.pairs.astype(np.int64)))
73
-
74
- # face_id → index nel ModuleList `blocks`
75
- face_to_slot = np.full(mesh.num_faces, -1, dtype=np.int64)
76
- blocks: List[TriangleBlock] = []
77
-
78
- for fa, fb in mesh.pairs:
79
- face_to_slot[int(fa)] = len(blocks)
80
- blocks.append(TriangleBlock(d_model, d_hidden))
81
- face_to_slot[int(fb)] = len(blocks)
82
- blocks.append(TriangleBlock(d_model, d_hidden))
83
-
84
- for u in mesh.unpaired:
85
- face_to_slot[int(u)] = len(blocks)
86
- blocks.append(TriangleBlock(d_model, d_hidden))
87
-
88
- self.blocks = nn.ModuleList(blocks)
89
- self.register_buffer("face_to_slot", torch.from_numpy(face_to_slot))
90
-
91
- def forward_face(self, face_id: int, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
92
- slot = int(self.face_to_slot[face_id].item())
93
- return self.blocks[slot](x)
94
-
95
-
96
- class CausalSelfAttention(nn.Module):
97
- """Multi-head self-attention con maschera causale (lookahead vietato)."""
98
-
99
- def __init__(self, d_model: int, n_heads: int, dropout: float = 0.0):
100
- super().__init__()
101
- if d_model % n_heads != 0:
102
- raise ValueError(f"d_model={d_model} non divisibile per n_heads={n_heads}")
103
- self.d_model = d_model
104
- self.n_heads = n_heads
105
- self.head_dim = d_model // n_heads
106
- self.qkv = nn.Linear(d_model, 3 * d_model, bias=False)
107
- self.proj = nn.Linear(d_model, d_model, bias=False)
108
- self.attn_drop = nn.Dropout(dropout)
109
- self.resid_drop = nn.Dropout(dropout)
110
-
111
- def forward(self, x: torch.Tensor) -> torch.Tensor:
112
- # x: (B, T, d)
113
- b, t, _ = x.shape
114
- qkv = self.qkv(x).view(b, t, 3, self.n_heads, self.head_dim)
115
- qkv = qkv.permute(2, 0, 3, 1, 4) # (3, B, H, T, hd)
116
- q, k, v = qkv[0], qkv[1], qkv[2]
117
-
118
- scale = 1.0 / math.sqrt(self.head_dim)
119
- # PyTorch SDPA con is_causal=True (Flash/mem-efficient quando disponibili)
120
- y = F.scaled_dot_product_attention(
121
- q, k, v,
122
- attn_mask=None,
123
- dropout_p=self.attn_drop.p if self.training else 0.0,
124
- is_causal=True,
125
- scale=scale,
126
- ) # (B, H, T, hd)
127
- y = y.transpose(1, 2).contiguous().view(b, t, self.d_model)
128
- return self.resid_drop(self.proj(y))
129
-
130
-
131
- class CausalAttnBlock(nn.Module):
132
- """Pre-norm: LayerNorm → causal attn → residual → LayerNorm → FF → residual."""
133
-
134
- def __init__(
135
- self,
136
- d_model: int,
137
- n_heads: int,
138
- d_ff: Optional[int] = None,
139
- dropout: float = 0.0,
140
- ):
141
- super().__init__()
142
- h = d_ff or (4 * d_model)
143
- self.ln1 = nn.LayerNorm(d_model)
144
- self.attn = CausalSelfAttention(d_model, n_heads, dropout=dropout)
145
- self.ln2 = nn.LayerNorm(d_model)
146
- self.ff = nn.Sequential(
147
- nn.Linear(d_model, h),
148
- nn.GELU(),
149
- nn.Linear(h, d_model),
150
- nn.Dropout(dropout),
151
- )
152
-
153
- def forward(self, x: torch.Tensor) -> torch.Tensor:
154
- x = x + self.attn(self.ln1(x))
155
- x = x + self.ff(self.ln2(x))
156
- return x
157
-
158
-
159
- def _default_n_heads(d_model: int) -> int:
160
- for h in (8, 4, 2, 1):
161
- if d_model % h == 0:
162
- return h
163
- return 1
164
-
165
-
166
- class SphericalMeshModel(nn.Module):
167
- """
168
- LM: token → emb (+pos) → CausalAttn × N → walk mesh parallelo (B*T) → vocab.
169
-
170
- Contesto temporale via attenzione causale (no carry sequenziale).
171
- """
172
-
173
- def __init__(
174
- self,
175
- vocab_size: int,
176
- d_model: int = 64,
177
- frequency: int = 2,
178
- walk: Optional[WalkConfig] = None,
179
- d_hidden: Optional[int] = None,
180
- n_attn_layers: int = 2,
181
- n_heads: Optional[int] = None,
182
- max_seq_len: int = 512,
183
- use_mesh_walk: bool = True,
184
- attn_dropout: float = 0.0,
185
- compass_spiral: bool = False,
186
- ):
187
- super().__init__()
188
- self.compass_spiral = bool(compass_spiral)
189
- self.mesh: GeodesicMesh = build_geodesic_mesh(
190
- frequency, compass_spiral=self.compass_spiral
191
- )
192
- self.walk = walk or WalkConfig()
193
- self.d_model = d_model
194
- self.vocab_size = vocab_size
195
- self.n_attn_layers = int(n_attn_layers)
196
- self.use_mesh_walk = bool(use_mesh_walk)
197
- self.max_seq_len = int(max_seq_len)
198
- heads = int(n_heads) if n_heads is not None else _default_n_heads(d_model)
199
-
200
- self.tok_emb = nn.Embedding(vocab_size, d_model)
201
- nn.init.normal_(self.tok_emb.weight, mean=0.0, std=0.02)
202
- self.pos_emb = nn.Embedding(self.max_seq_len, d_model)
203
- nn.init.normal_(self.pos_emb.weight, mean=0.0, std=0.02)
204
-
205
- self.attn_blocks = nn.ModuleList(
206
- [
207
- CausalAttnBlock(d_model, heads, d_ff=d_hidden or (4 * d_model), dropout=attn_dropout)
208
- for _ in range(self.n_attn_layers)
209
- ]
210
- )
211
-
212
- # Proietta embedding → direzione 3D sulla sfera (faccia di ingresso)
213
- self.to_sphere = nn.Linear(d_model, 3)
214
- self.bank = PairedFaceBank(self.mesh, d_model, d_hidden)
215
- self.final_norm = nn.LayerNorm(d_model)
216
- self.lm_head = nn.Linear(d_model, vocab_size, bias=False)
217
- # embedding tied (come goTommy2)
218
- self.lm_head.weight = self.tok_emb.weight
219
- self.emb_scale = d_model**-0.5
220
-
221
- # Buffer geometria per nearest face su GPU/CPU
222
- self.register_buffer(
223
- "centroids",
224
- torch.from_numpy(self.mesh.face_centroids.astype(np.float32)),
225
- )
226
- self.register_buffer(
227
- "adjacency",
228
- torch.from_numpy(self.mesh.adjacency.astype(np.int64)),
229
- )
230
-
231
- def project_to_face(self, h: torch.Tensor) -> torch.Tensor:
232
- """h (..., d) → face ids (...)"""
233
- direction = self.to_sphere(h)
234
- direction = F.normalize(direction, dim=-1)
235
- # (..., F)
236
- scores = direction @ self.centroids.T
237
- return scores.argmax(dim=-1)
238
-
239
- def _route_step(
240
- self,
241
- face: torch.Tensor,
242
- logits: torch.Tensor,
243
- ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
244
- """
245
- face: (B,) long — faccia corrente
246
- logits: (B, 4) — 3 lati + exit
247
- returns: next_face (B,), exit_mask (B,) bool, side_choice (B,) in {0,1,2,-1}
248
- """
249
- temp = max(self.walk.router_temperature, 1e-6)
250
- probs = F.softmax(logits / temp, dim=-1)
251
- exit_prob = probs[:, 3]
252
- side_probs = probs[:, :3]
253
-
254
- if self.walk.hard_routing:
255
- side = side_probs.argmax(dim=-1)
256
- else:
257
- # Gumbel-softmax straight-through sui 3 lati
258
- side = F.gumbel_softmax(logits[:, :3] / temp, hard=True, dim=-1).argmax(dim=-1)
259
-
260
- exit_mask = exit_prob >= self.walk.exit_threshold
261
- # lookup vettoriale: adjacency[face, side]
262
- next_face = self.adjacency[face.long(), side.long()]
263
- side_out = torch.where(exit_mask, torch.full_like(side, -1), side)
264
- return next_face, exit_mask, side_out
265
-
266
- def walk_token(
267
- self,
268
- h: torch.Tensor,
269
- start_face: Optional[torch.Tensor] = None,
270
- record_path: bool = False,
271
- ) -> Tuple[torch.Tensor, Optional[List[List[int]]]]:
272
- """
273
- h: (B, d) stato iniziale token — tipicamente B*T flattened
274
- returns: h_final (B, d), paths opzionale list per batch
275
- """
276
- b = h.shape[0]
277
- face = start_face if start_face is not None else self.project_to_face(h)
278
- paths: Optional[List[List[int]]] = [[int(face[i].item())] for i in range(b)] if record_path else None
279
- active = torch.ones(b, dtype=torch.bool, device=h.device)
280
-
281
- for _ in range(self.walk.max_hops):
282
- if not active.any():
283
- break
284
- # Esegue i blocchi per-faccia (batch misto: loop su facce uniche)
285
- logits = h.new_zeros(b, 4)
286
- h_new = h.clone()
287
- unique_faces = face[active].unique()
288
- for f_id in unique_faces.tolist():
289
- mask = active & (face == f_id)
290
- if not mask.any():
291
- continue
292
- out, lg = self.bank.forward_face(int(f_id), h[mask])
293
- h_new[mask] = out
294
- logits[mask] = lg
295
- h = h_new
296
-
297
- next_face, exit_mask, _ = self._route_step(face, logits)
298
- # Solo i token ancora attivi possono uscire / muoversi
299
- moved = active & ~exit_mask
300
- face = torch.where(moved, next_face, face)
301
- just_exit = active & exit_mask
302
- active = active & ~exit_mask
303
-
304
- if record_path and paths is not None:
305
- for i in range(b):
306
- if moved[i]:
307
- paths[i].append(int(face[i].item()))
308
- elif just_exit[i]:
309
- paths[i].append(-1) # sentinel exit
310
-
311
- return h, paths
312
-
313
- def forward(
314
- self,
315
- input_ids: torch.Tensor,
316
- record_path: bool = False,
317
- ) -> Tuple[torch.Tensor, Optional[List[List[List[int]]]]]:
318
- """
319
- input_ids: (B, T)
320
- returns: logits (B, T, V), paths [B][T][hops] se richiesto
321
-
322
- Pipeline:
323
- emb(+pos) → CausalAttnBlock × N → walk_token(B*T) → final_norm → lm_head
324
- """
325
- b, t = input_ids.shape
326
- if t > self.max_seq_len:
327
- raise ValueError(f"seq_len={t} > max_seq_len={self.max_seq_len}")
328
-
329
- pos = torch.arange(t, device=input_ids.device).unsqueeze(0) # (1, T)
330
- h = self.tok_emb(input_ids) * self.emb_scale + self.pos_emb(pos)
331
-
332
- for block in self.attn_blocks:
333
- h = block(h) # (B, T, d)
334
-
335
- paths: Optional[List[List[List[int]]]] = None
336
- if self.use_mesh_walk:
337
- flat = h.reshape(b * t, self.d_model)
338
- flat_out, paths_flat = self.walk_token(flat, record_path=record_path)
339
- h = flat_out.view(b, t, self.d_model)
340
- if record_path and paths_flat is not None:
341
- # paths_flat[bi*T + ti] → paths[bi][ti]
342
- paths = [
343
- [paths_flat[bi * t + ti] for ti in range(t)]
344
- for bi in range(b)
345
- ]
346
-
347
- h = self.final_norm(h)
348
- logits = self.lm_head(h)
349
- # soft-cap: evita CE esplosa con residual walk + tied emb
350
- logits = 30.0 * torch.tanh(logits / 30.0)
351
- return logits, paths
352
-
353
-
354
- def count_parameters(model: nn.Module) -> int:
355
- return sum(p.numel() for p in model.parameters() if p.requires_grad)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
geodesic_stages_model.py DELETED
@@ -1,1616 +0,0 @@
1
- # ============================================================================
2
- # LEGAL NOTICE & WATERMARK: R08 NON-COMMERCIAL RESEARCH & ETHICAL LICENSE v2.0
3
- # Copyright (c) 2026 Thomas Seri (Sentinel). All Rights Reserved.
4
- # Exclusive Jurisdiction: Court of Milan (Sezione Specializzata Impresa), Italy.
5
- # Clean-room reverse engineering, distillation, commercial or military use
6
- # is strictly prohibited and constitutes a breach of contract.
7
- # ============================================================================
8
- __author__ = "Thomas Seri (Sentinel)"
9
- __license__ = "R08-Non-Commercial-Ethical-License-v2.0"
10
- __jurisdiction__ = "Court of Milan, Italy"
11
-
12
- """
13
- Cascade a 3 stadi in SERIE (non stream paralleli).
14
-
15
- Architettura (allineata alla timeline pezzi, grain più fine):
16
-
17
- pezzi_ids → emb_v1 (+pos) → CausalAttn_1 → mesh_walk_1 (=h1)
18
- → LN(proj_12(h1)) + LN(emb_v2)[*] → fuse_norm2 → mesh_walk_2 (=h2)
19
- → LN(proj_23(h2)) + LN(emb_v3)[*] → fuse_norm3 → mesh_walk_3 (=h3)
20
- → Drop0.1(n1(h1))+n2(h2)+n3(h3) → CausalAttn_2 → final_norm → lm_head (tied weight + bias)
21
-
22
- [*] v8.1 ``fuse_mask_pad``: su pad/unk V2/V3 l'addendo emb è azzerato (× active);
23
- ``mask_pad_stages`` già saltava solo walk/piramide.
24
-
25
- Le sequenze parole/unioni sono espanse sulla lunghezza pezzi (pad dove non
26
- c'è parola/unione). Obiettivo LM: next-token sui pezzi (unità di decode).
27
-
28
- Router v6 (``router_v6=True``, default spento = v5 invariato), per stadio:
29
- - ingresso: faccia = argmax(coseni + face_bias) in training e in eval;
30
- face_bias (buffer, niente gradiente) segue il Loss-Free Balancing
31
- (``update_face_bias`` dopo ogni step); peso straight-through
32
- w = p_f / sg(p_f) con p = softmax(coseni · exp(logit_scale)), logit_scale
33
- appresa (init ln 10, clamp ≤ ln 100);
34
- - ogni salto: x + w · ff(norm(x)), il peso non tocca mai il residuo;
35
- - porte: come v5 (Gumbel-ST in training, argmax in eval);
36
- - uscita: decisione dura a soglia nel forward; il peso del salto successivo
37
- è w_porta · p_stay / sg(p_stay), p_stay = 1 − probs[:, uscita], così il
38
- logit di uscita riceve gradiente dal contributo dell'esperto che segue.
39
- ``hash_routing=True``: come v6 ma faccia d'ingresso fissa per id del token
40
- (pezzo / parola / unione nei tre stadi, ``hash_faces``), senza to_sphere,
41
- logit_scale né face_bias.
42
- ``no_walk=True`` (solo v6/hash, ``max_hops=1``): controllo del cammino, ogni
43
- token usa solo il blocco della faccia d'ingresso in ogni stadio; dopo quel
44
- blocco escono tutti e le porte non vengono valutate (niente rumore Gumbel).
45
- ``walk_to_center=True`` (v10 Spoke-to-Hub, solo hash, ``H_max∈{2,3}``):
46
- ingresso Zipf/hash invariato (periferia = ID); dopo ogni blocco il token
47
- segue ``parent_face[f]`` verso un **anello polare hub** di ``k_core≈15–20``
48
- facce (non una sola faccia). Gate (Argon):
49
- hop0 residuo pieno (come v8.1, ``g=None``);
50
- hop≥1 ``g = (c + (1−c)·σ(Linear(x))) / √H_max`` con ``c=center_mix``
51
- (default 0.25, formula liscia). Bias gate init 0. Niente Gumbel;
52
- pad/unk = 0 hop (mask-pad). Train==eval path.
53
- La piramide (se attiva) resta sulla faccia d'ingresso Zipf.
54
- ``sector_mix=True`` (v11, implica ``walk_to_center``): stesso albero parent→hub,
55
- ma hop≥1 non è più spoke sottile solo-parent: ``x ← x + g · mean_{f∈settore(entry)}(Δ_f)``
56
- dove il settore è lo **spicchio azimutale** di ``K≈sector_frac·F`` facce
57
- (default 1/4) intorno all'ingresso Zipf. Hop0 resta solo faccia entry a peso 1.0.
58
- Niente layer attention, niente Gumbel, Zipf classico (no multi-slot).
59
- ``sheet_mix=True`` (v13 Sheet-Mix / v14 pezzi-only, implica ``sector_mix`` path): stesso hop0/hop≥1
60
- e ``_apply_sector_mean``, ma il buffer ``sector_faces`` è ``(F, K_eff=12)`` =
61
- 3 fogli complementari (cerchi massimi a 0°/90°/±45°, esclusa l'orientazione
62
- ``spiral_rank % 4``) × ``m=4`` facce equidistanti per foglio (ponte polo-a-polo).
63
- Iso-param rispetto a v11 (solo indici; weight sharing invariato).
64
- **Storico (v13–v18, default):** hop≥1 = media Δ sui blocchi delle K_eff facce dei fogli
65
- (cross-face). **``sheet_entry_only=True`` (v19):** hop≥1 applica solo il blocco della
66
- faccia d'ingresso Zipf (niente media su altre facce); i fogli non toccano altri esperti.
67
- Il cammino parent→hub resta per lo stop; piramide invariata sull'entry.
68
- ``pyramid_center=True`` (solo hash, «sfera a spicchi piramidali»): dopo il
69
- cammino di ogni stadio il token scende nella piramide della sua faccia
70
- d'ingresso, un passaggio d → d/2 con proiezione condivisa e guadagno/spostamento
71
- per faccia, e arriva al centro, un MLP denso condiviso dallo stadio; l'uscita
72
- del centro si somma al ramo della faccia (``PyramidCenter``).
73
- """
74
- from __future__ import annotations
75
-
76
- import math
77
- from collections import deque
78
- from pathlib import Path
79
- from typing import List, Optional, Tuple, Union
80
-
81
- import numpy as np
82
- import torch
83
- import torch.nn as nn
84
- import torch.nn.functional as F
85
-
86
- from fast_walk import (
87
- _build_paths,
88
- fast_walk_token,
89
- fast_walk_token_v6,
90
- grouped_block_forward,
91
- stack_bank,
92
- )
93
- from geodesic_network import (
94
- CausalAttnBlock,
95
- PairedFaceBank,
96
- WalkConfig,
97
- _default_n_heads,
98
- )
99
- from mesh_topology import GeodesicMesh, build_geodesic_mesh, icosahedron_vertices
100
-
101
- DEFAULT_K_CORE = 18 # anello polare hub @ F=180 (~10%); clamp a [15, 20] tipico
102
- V10_HOPS_DEFAULT = 2 # H_max=2; CLI può alzare a 3
103
- DEFAULT_CENTER_MIX = 0.25 # hop≥1: g=(c+(1-c)σ(Wx))/√H; hop0=pieno
104
- DEFAULT_SECTOR_FRAC = 0.25 # v11: fetta azimutale ~1/4 di F (~45 @ F=180)
105
- DEFAULT_EDGE_NB_TAU = 4.0 # v12: softmax(τ · z_polo) sui 3 lati
106
- DEFAULT_SHEET_M = 4 # v13: facce per foglio complementare (K_eff = 3·m = 12)
107
-
108
-
109
- def build_face_edge_ids(faces: np.ndarray) -> Tuple[np.ndarray, int]:
110
- """``face_edges[f, i]`` = id unico del lato i; ``E = 3F/2`` su mesh chiusa."""
111
- fcount = int(faces.shape[0])
112
- edge_to_id: dict = {}
113
- face_edges = np.zeros((fcount, 3), dtype=np.int64)
114
- next_id = 0
115
- for fi, (a, b, c) in enumerate(faces):
116
- for li, (u, v) in enumerate(((int(a), int(b)), (int(b), int(c)), (int(c), int(a)))):
117
- key = (u, v) if u < v else (v, u)
118
- eid = edge_to_id.get(key)
119
- if eid is None:
120
- eid = next_id
121
- edge_to_id[key] = eid
122
- next_id += 1
123
- face_edges[fi, li] = eid
124
- return face_edges, next_id
125
-
126
-
127
- def build_edge_neighbor_weights(
128
- adjacency: np.ndarray,
129
- face_centroids: np.ndarray,
130
- tau: float = DEFAULT_EDGE_NB_TAU,
131
- ) -> np.ndarray:
132
- """Pesi (F, 3) sui vicini di lato: softmax(τ · z_polo[nb]), verso il Polo Nord."""
133
- z = face_centroids[:, 2].astype(np.float64)
134
- scores = z[adjacency] * float(tau) # (F, 3)
135
- scores = scores - scores.max(axis=1, keepdims=True)
136
- e = np.exp(scores)
137
- return (e / e.sum(axis=1, keepdims=True)).astype(np.float32)
138
-
139
-
140
- def build_spoke_hub_parent(
141
- adjacency: np.ndarray,
142
- face_centroids: np.ndarray,
143
- k_core: int = DEFAULT_K_CORE,
144
- ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
145
- """Albero Spoke→Hub: ``parent[f]`` verso l'anello polare di ``k_core`` facce.
146
-
147
- Hub = top-``k_core`` facce per proiezione sull'asse polare icosaedrico
148
- (vertice 0, stesso di ``compass_spiral``). BFS multi-sorgente: ogni faccia
149
- non-hub punta al vicino più vicino all'anello; sulle hub ``parent[h]=h``.
150
-
151
- Ritorna ``(parent_face (F,), is_hub (F,) bool, hub_ids (K,))``.
152
- """
153
- n = int(adjacency.shape[0])
154
- k = int(max(1, min(int(k_core), n)))
155
- pole = icosahedron_vertices()[0]
156
- u3 = pole / np.linalg.norm(pole)
157
- z = face_centroids.astype(np.float64) @ u3
158
- hub_ids = np.argsort(-z)[:k].astype(np.int64)
159
- is_hub = np.zeros(n, dtype=bool)
160
- is_hub[hub_ids] = True
161
- parent = np.arange(n, dtype=np.int64)
162
- dist = np.full(n, -1, dtype=np.int32)
163
- q: deque = deque()
164
- for h in hub_ids.tolist():
165
- dist[h] = 0
166
- parent[h] = h
167
- q.append(h)
168
- while q:
169
- f = q.popleft()
170
- for nb in adjacency[f].tolist():
171
- nb = int(nb)
172
- if dist[nb] < 0:
173
- dist[nb] = int(dist[f]) + 1
174
- parent[nb] = f
175
- q.append(nb)
176
- if int((dist < 0).sum()) > 0:
177
- raise RuntimeError("spoke-hub: grafo facce non connesso")
178
- return parent, is_hub, hub_ids
179
-
180
-
181
- def build_azimuth_sectors(
182
- face_centroids: np.ndarray,
183
- sector_frac: float = DEFAULT_SECTOR_FRAC,
184
- ) -> np.ndarray:
185
- """Settore azimutale per faccia: spicchio verso il polo (asse icosaedrico 0).
186
-
187
- Per ogni faccia ``e``, le ``K = max(1, round(sector_frac · F))`` facce con
188
- minore distanza circolare in azimut intorno al polo (stesso vertice 0 di
189
- ``build_spoke_hub_parent`` / ``compass_spiral``). Con ``sector_frac=0.25`` e
190
- F=180 → K≈45 (~1/4 di sfera in longitudine, tutte le latitudini).
191
-
192
- Ritorna ``sector_faces (F, K)`` int64; ``sector_faces[e, 0] == e``.
193
- """
194
- if not (0.0 < float(sector_frac) <= 1.0):
195
- raise ValueError("sector_frac deve essere in (0, 1]")
196
- cents = face_centroids.astype(np.float64)
197
- n = int(cents.shape[0])
198
- k = int(max(1, min(n, round(float(sector_frac) * n))))
199
- pole = icosahedron_vertices()[0]
200
- u3 = pole / np.linalg.norm(pole)
201
- # base ortonormale nel piano equatoriale
202
- ref = np.array([1.0, 0.0, 0.0], dtype=np.float64)
203
- if abs(float(ref @ u3)) > 0.9:
204
- ref = np.array([0.0, 1.0, 0.0], dtype=np.float64)
205
- e1 = ref - (ref @ u3) * u3
206
- e1 = e1 / np.linalg.norm(e1)
207
- e2 = np.cross(u3, e1)
208
- xy = cents - np.outer(cents @ u3, u3)
209
- x = xy @ e1
210
- y = xy @ e2
211
- az = np.arctan2(y, x) # (-π, π]; poli (xy≈0) → 0
212
- # distanza circolare |Δφ| ∈ [0, π]
213
- dphi = np.abs(az[:, None] - az[None, :])
214
- dphi = np.minimum(dphi, 2.0 * np.pi - dphi)
215
- # self sempre in slot 0 (al polo più facce condividono φ≈0 → argsort non basta)
216
- np.fill_diagonal(dphi, -1.0)
217
- order = np.argsort(dphi, axis=1, kind="mergesort")[:, :k].astype(np.int64)
218
- return order
219
-
220
-
221
- def build_complementary_sheets(
222
- face_centroids: np.ndarray,
223
- spiral_rank: Optional[np.ndarray] = None,
224
- m_per_sheet: int = DEFAULT_SHEET_M,
225
- ) -> np.ndarray:
226
- """v13 Sheet-Mix: 3 fogli complementari × m facce su cerchi massimi → (F, 3m).
227
-
228
- Per ogni faccia ``f`` costruisce la base tangente ``(t_par, t_mer)`` rispetto
229
- al polo icosaedrico (vertice 0). Le 4 normali ai cerchi massimi a
230
- 0° / 90° / +45° / −45° sono::
231
-
232
- N_V = t_par (meridiano, polo↔polo)
233
- N_H = t_mer (ortogonale in f)
234
- N_+ = (t_par+t_mer)/√2
235
- N_- = (t_par-t_mer)/√2
236
-
237
- L'orientazione propria è ``spiral_rank[f] % 4`` (se assente → 0); si usano
238
- i **3 fogli complementari**. Su ciascun foglio si campionano ``m`` facce
239
- equidistanti sul cerchio massimo (θ = 2πk/m), incluso il ponte
240
- fronte/retro e polo-a-polo. Drop-in su ``sector_faces`` / ``_apply_sector_mean``.
241
- """
242
- m = int(m_per_sheet)
243
- if m < 1:
244
- raise ValueError("m_per_sheet deve essere >= 1")
245
- cents = face_centroids.astype(np.float64)
246
- n = int(cents.shape[0])
247
- # normalizza (centroidi mesh già unitari, ma per sicurezza)
248
- norms = np.linalg.norm(cents, axis=1, keepdims=True)
249
- cents = cents / np.maximum(norms, 1e-12)
250
- pole = icosahedron_vertices()[0]
251
- u3 = pole / np.linalg.norm(pole)
252
- # asse equatoriale fisso per i poli (stesso schema di build_azimuth_sectors)
253
- ref = np.array([1.0, 0.0, 0.0], dtype=np.float64)
254
- if abs(float(ref @ u3)) > 0.9:
255
- ref = np.array([0.0, 1.0, 0.0], dtype=np.float64)
256
- e1 = ref - (ref @ u3) * u3
257
- e1 = e1 / np.linalg.norm(e1)
258
-
259
- if spiral_rank is None:
260
- orient = np.zeros(n, dtype=np.int64)
261
- else:
262
- sr = np.asarray(spiral_rank, dtype=np.int64).reshape(-1)
263
- if sr.shape[0] != n:
264
- raise ValueError(f"spiral_rank len={sr.shape[0]} ≠ F={n}")
265
- orient = np.mod(sr, 4).astype(np.int64)
266
-
267
- out = np.empty((n, 3 * m), dtype=np.int64)
268
- # precompute all pairwise cosines for nearest-face lookup
269
- # (F=180 → F² cheap)
270
- for f in range(n):
271
- c = cents[f]
272
- # t_par ∝ u3 × c (parallelo); ai poli |u3×c|→0 → e1
273
- t_par = np.cross(u3, c)
274
- tp_n = float(np.linalg.norm(t_par))
275
- if tp_n < 1e-8:
276
- t_par = e1.copy()
277
- else:
278
- t_par = t_par / tp_n
279
- t_mer = np.cross(c, t_par)
280
- tm_n = float(np.linalg.norm(t_mer))
281
- if tm_n < 1e-8:
282
- t_mer = np.cross(t_par, u3)
283
- t_mer = t_mer / max(float(np.linalg.norm(t_mer)), 1e-12)
284
- else:
285
- t_mer = t_mer / tm_n
286
- normals = (
287
- t_par, # 0 Verticale
288
- t_mer, # 1 Orizzontale
289
- (t_par + t_mer) / math.sqrt(2.0), # 2 D+
290
- (t_par - t_mer) / math.sqrt(2.0), # 3 D-
291
- )
292
- own = int(orient[f])
293
- comps = [s for s in range(4) if s != own]
294
- col = 0
295
- for s in comps:
296
- N = normals[s]
297
- # base ortonormale nel piano del cerchio massimo (⊥ N), con ea ≈ c
298
- ea = c - (c @ N) * N
299
- ea_n = float(np.linalg.norm(ea))
300
- if ea_n < 1e-8:
301
- # c ∥ N (non dovrebbe succedere: N costruita ⊥ c)
302
- ea = t_par if s != 0 else t_mer
303
- ea = ea - (ea @ N) * N
304
- ea = ea / max(float(np.linalg.norm(ea)), 1e-12)
305
- else:
306
- ea = ea / ea_n
307
- eb = np.cross(N, ea)
308
- eb = eb / max(float(np.linalg.norm(eb)), 1e-12)
309
- for k in range(m):
310
- theta = 2.0 * math.pi * float(k) / float(m)
311
- target = math.cos(theta) * ea + math.sin(theta) * eb
312
- # faccia col centroide più allineato a target
313
- scores = cents @ target
314
- out[f, col] = int(np.argmax(scores))
315
- col += 1
316
- # slot 0 = entry (coerente con build_azimuth_sectors)
317
- out[f, 0] = f
318
- return out
319
-
320
-
321
- V6_LOGIT_SCALE_INIT = math.log(10.0)
322
- V6_LOGIT_SCALE_MAX = math.log(100.0)
323
- V6_BIAS_RATE = 1e-3
324
- _HASH_SALTS = (0x9E3779B9, 0x85EBCA6B, 0xC2B2AE35)
325
- PYRAMID_SEED_OFFSET = 7919 # seme del generatore delle piramidi = seme torch + offset
326
-
327
-
328
- def _lowbias32(x: int) -> int:
329
- x &= 0xFFFFFFFF
330
- x ^= x >> 16
331
- x = (x * 0x7FEB352D) & 0xFFFFFFFF
332
- x ^= x >> 15
333
- x = (x * 0x846CA68B) & 0xFFFFFFFF
334
- x ^= x >> 16
335
- return x
336
-
337
-
338
- def hash_faces(vocab_size: int, n_faces: int, stage: int) -> torch.Tensor:
339
- """Faccia d'ingresso fissa per id (--hash-routing): lowbias32(id XOR sale dello stadio) mod n_faces."""
340
- salt = _HASH_SALTS[stage - 1]
341
- return torch.tensor([_lowbias32(i ^ salt) % n_faces for i in range(vocab_size)], dtype=torch.long)
342
-
343
-
344
- class PyramidCenter(nn.Module):
345
- """Piramidi sotto le facce di uno stadio e centro condiviso (--pyramid-center).
346
-
347
- Discesa dalla faccia f: z = (1 + face_gain[f]) ⊙ down(norm(x)) + face_shift[f], d → d_narrow;
348
- la proiezione propria della faccia è diag(1 + face_gain[f]) · down. Centro, uguale per tutti
349
- i token: c = up(GELU(inner(z))), d_narrow → d_hidden → d. Uscita x + c; up parte da zero, quindi
350
- all'inizio l'uscita è x.
351
- """
352
-
353
- def __init__(
354
- self, d_model: int, n_faces: int, d_narrow: int, d_hidden: int, generator: Optional[torch.Generator] = None
355
- ):
356
- super().__init__()
357
- self.norm = nn.LayerNorm(d_model)
358
- self.down = nn.Linear(d_model, d_narrow, bias=False)
359
- self.face_gain = nn.Parameter(torch.zeros(n_faces, d_narrow))
360
- self.face_shift = nn.Parameter(torch.zeros(n_faces, d_narrow))
361
- self.center = nn.Sequential(nn.Linear(d_narrow, d_hidden), nn.GELU(), nn.Linear(d_hidden, d_model))
362
- self.reset_parameters(generator)
363
-
364
- def reset_parameters(self, generator: Optional[torch.Generator] = None) -> None:
365
- """Init di nn.Linear per down e per il primo strato del centro; ultima proiezione, guadagni e spostamenti a zero."""
366
- for lin in (self.down, self.center[0]):
367
- nn.init.kaiming_uniform_(lin.weight, a=math.sqrt(5), generator=generator)
368
- if lin.bias is not None:
369
- bound = 1.0 / math.sqrt(lin.in_features)
370
- nn.init.uniform_(lin.bias, -bound, bound, generator=generator)
371
- for p in (self.center[2].weight, self.center[2].bias, self.face_gain, self.face_shift):
372
- nn.init.zeros_(p)
373
- self.norm.reset_parameters()
374
-
375
- def forward(self, x: torch.Tensor, face: torch.Tensor) -> torch.Tensor:
376
- z = self.down(self.norm(x))
377
- z = z * (1.0 + F.embedding(face, self.face_gain)) + F.embedding(face, self.face_shift)
378
- return x + self.center(z)
379
-
380
-
381
- class HexPyramidCenter(nn.Module):
382
- """v9: guadagno/spostamento per **vertice** della mesh; media sui 3 vertici della faccia.
383
-
384
- Stessa discesa d → d_narrow e centro MLP condiviso di ``PyramidCenter``, ma i parametri
385
- locali vivono sui V vertici (esagoni/pentagoni duali) invece che sulle F facce: facce
386
- adiacenti condividono i vertici e quindi parte della piramide.
387
- """
388
-
389
- def __init__(
390
- self,
391
- d_model: int,
392
- n_vertices: int,
393
- face_vertices: torch.Tensor,
394
- d_narrow: int,
395
- d_hidden: int,
396
- generator: Optional[torch.Generator] = None,
397
- ):
398
- super().__init__()
399
- if face_vertices.ndim != 2 or face_vertices.shape[1] != 3:
400
- raise ValueError(f"face_vertices deve essere (F, 3), got {tuple(face_vertices.shape)}")
401
- self.norm = nn.LayerNorm(d_model)
402
- self.down = nn.Linear(d_model, d_narrow, bias=False)
403
- self.vertex_gain = nn.Parameter(torch.zeros(n_vertices, d_narrow))
404
- self.vertex_shift = nn.Parameter(torch.zeros(n_vertices, d_narrow))
405
- self.center = nn.Sequential(nn.Linear(d_narrow, d_hidden), nn.GELU(), nn.Linear(d_hidden, d_model))
406
- self.register_buffer("face_vertices", face_vertices.long().contiguous())
407
- self.reset_parameters(generator)
408
-
409
- def reset_parameters(self, generator: Optional[torch.Generator] = None) -> None:
410
- for lin in (self.down, self.center[0]):
411
- nn.init.kaiming_uniform_(lin.weight, a=math.sqrt(5), generator=generator)
412
- if lin.bias is not None:
413
- bound = 1.0 / math.sqrt(lin.in_features)
414
- nn.init.uniform_(lin.bias, -bound, bound, generator=generator)
415
- for p in (self.center[2].weight, self.center[2].bias, self.vertex_gain, self.vertex_shift):
416
- nn.init.zeros_(p)
417
- self.norm.reset_parameters()
418
-
419
- def forward(self, x: torch.Tensor, face: torch.Tensor) -> torch.Tensor:
420
- z = self.down(self.norm(x))
421
- verts = self.face_vertices[face] # (..., 3)
422
- g = F.embedding(verts, self.vertex_gain).mean(dim=-2)
423
- s = F.embedding(verts, self.vertex_shift).mean(dim=-2)
424
- z = z * (1.0 + g) + s
425
- return x + self.center(z)
426
-
427
-
428
- class SharedEdgeVertexPyramid(nn.Module):
429
- """v12: face_gain privato + lati (E) e vertici (V) condivisi + 1 centro MLP.
430
-
431
- ``z`` modulato da ``1 + face + 0.5·mean(edge) + 0.5·mean(vertex)`` (edge/vert init 0).
432
- """
433
-
434
- def __init__(
435
- self,
436
- d_model: int,
437
- n_faces: int,
438
- n_edges: int,
439
- n_vertices: int,
440
- face_edges: torch.Tensor,
441
- face_vertices: torch.Tensor,
442
- d_narrow: int,
443
- d_hidden: int,
444
- generator: Optional[torch.Generator] = None,
445
- ):
446
- super().__init__()
447
- if face_edges.ndim != 2 or face_edges.shape[1] != 3:
448
- raise ValueError(f"face_edges deve essere (F, 3), got {tuple(face_edges.shape)}")
449
- if face_vertices.ndim != 2 or face_vertices.shape[1] != 3:
450
- raise ValueError(f"face_vertices deve essere (F, 3), got {tuple(face_vertices.shape)}")
451
- self.norm = nn.LayerNorm(d_model)
452
- self.down = nn.Linear(d_model, d_narrow, bias=False)
453
- self.face_gain = nn.Parameter(torch.zeros(n_faces, d_narrow))
454
- self.face_shift = nn.Parameter(torch.zeros(n_faces, d_narrow))
455
- self.edge_gain = nn.Parameter(torch.zeros(n_edges, d_narrow))
456
- self.edge_shift = nn.Parameter(torch.zeros(n_edges, d_narrow))
457
- self.vertex_gain = nn.Parameter(torch.zeros(n_vertices, d_narrow))
458
- self.vertex_shift = nn.Parameter(torch.zeros(n_vertices, d_narrow))
459
- self.center = nn.Sequential(nn.Linear(d_narrow, d_hidden), nn.GELU(), nn.Linear(d_hidden, d_model))
460
- self.register_buffer("face_edges", face_edges.long().contiguous())
461
- self.register_buffer("face_vertices", face_vertices.long().contiguous())
462
- self.reset_parameters(generator)
463
-
464
- def reset_parameters(self, generator: Optional[torch.Generator] = None) -> None:
465
- for lin in (self.down, self.center[0]):
466
- nn.init.kaiming_uniform_(lin.weight, a=math.sqrt(5), generator=generator)
467
- if lin.bias is not None:
468
- bound = 1.0 / math.sqrt(lin.in_features)
469
- nn.init.uniform_(lin.bias, -bound, bound, generator=generator)
470
- for p in (
471
- self.center[2].weight,
472
- self.center[2].bias,
473
- self.face_gain,
474
- self.face_shift,
475
- self.edge_gain,
476
- self.edge_shift,
477
- self.vertex_gain,
478
- self.vertex_shift,
479
- ):
480
- nn.init.zeros_(p)
481
- self.norm.reset_parameters()
482
-
483
- def forward(self, x: torch.Tensor, face: torch.Tensor) -> torch.Tensor:
484
- z = self.down(self.norm(x))
485
- f = face.long()
486
- eg = F.embedding(self.face_edges[f], self.edge_gain).mean(dim=-2)
487
- es = F.embedding(self.face_edges[f], self.edge_shift).mean(dim=-2)
488
- vg = F.embedding(self.face_vertices[f], self.vertex_gain).mean(dim=-2)
489
- vs = F.embedding(self.face_vertices[f], self.vertex_shift).mean(dim=-2)
490
- g = F.embedding(f, self.face_gain) + 0.5 * eg + 0.5 * vg
491
- s = F.embedding(f, self.face_shift) + 0.5 * es + 0.5 * vs
492
- z = z * (1.0 + g) + s
493
- return x + self.center(z)
494
-
495
-
496
- def pyramid_kwargs(blob: dict) -> dict:
497
- """Argomenti di CascadeMeshModel per la piramide, dai metadati di un checkpoint (assenti = spenta)."""
498
- if not blob.get("pyramid_center") and not blob.get("hex_pyramid") and not blob.get("shared_edges_verts"):
499
- return {}
500
- cfg = blob.get("pyramid_config") or {}
501
- out: dict = {
502
- "pyramid_center": True,
503
- "pyramid_narrow": cfg.get("d_stretto"),
504
- "pyramid_hidden": cfg.get("d_centro"),
505
- }
506
- if blob.get("hex_pyramid"):
507
- out["hex_pyramid"] = True
508
- if blob.get("shared_edges_verts"):
509
- out["shared_edges_verts"] = True
510
- return out
511
-
512
-
513
- def v8_kwargs(blob: dict) -> dict:
514
- """Argomenti v8.x di CascadeMeshModel dai metadati checkpoint (assenti = default v7).
515
-
516
- Lo zipf-hash è già nei buffer ``bank.hash_face`` dello state_dict: non serve
517
- ``zipf_hash_path`` in eval.
518
-
519
- ``fuse_mask_pad`` (v8.1): solo se presente nel blob — i ckpt v8.0 senza chiave
520
- restano senza mask sull'addendo emb in fuse (come in training).
521
- """
522
- out: dict = {}
523
- if blob.get("d_hidden") is not None:
524
- out["d_hidden"] = int(blob["d_hidden"])
525
- de = blob.get("d_emb_v23")
526
- if de is not None and int(de) > 0:
527
- out["d_emb_v23"] = int(de)
528
- if blob.get("inter_stage_attn"):
529
- out["inter_stage_attn"] = True
530
- if blob.get("mask_pad_stages"):
531
- out["mask_pad_stages"] = True
532
- out["pad_id_v2"] = int(blob.get("pad_id_v2", 3))
533
- out["unk_id_v2"] = int(blob.get("unk_id_v2", 4))
534
- out["pad_id_v3"] = int(blob.get("pad_id_v3", 3))
535
- out["unk_id_v3"] = int(blob.get("unk_id_v3", 4))
536
- if blob.get("fuse_mask_pad"):
537
- out["fuse_mask_pad"] = True
538
- if blob.get("multi_slot_v1"):
539
- out["multi_slot_v1"] = True
540
- return out
541
-
542
-
543
- def _make_emb(vocab: int, d_emb: int, d_model: int) -> nn.Module:
544
- """Embedding pieno a ``d_model``, oppure ``Embedding(d_emb)→Linear→d_model``."""
545
- if d_emb >= d_model:
546
- emb = nn.Embedding(vocab, d_model)
547
- nn.init.normal_(emb.weight, mean=0.0, std=0.02)
548
- return emb
549
- emb = nn.Embedding(vocab, d_emb)
550
- proj = nn.Linear(d_emb, d_model, bias=False)
551
- nn.init.normal_(emb.weight, mean=0.0, std=0.02)
552
- nn.init.normal_(proj.weight, mean=0.0, std=0.02)
553
- return nn.Sequential(emb, proj)
554
-
555
-
556
- class CascadeMeshModel(nn.Module):
557
- """Tre emb + tre mesh in catena (default), oppure solo-V1 (v1_only=True)."""
558
-
559
- def __init__(
560
- self,
561
- vocab_pezzi: int,
562
- vocab_parole: int = 0,
563
- vocab_unioni: int = 0,
564
- d_model: int = 256,
565
- frequency: int = 2,
566
- walk: Optional[WalkConfig] = None,
567
- d_hidden: Optional[int] = None,
568
- n_attn_layers: int = 2,
569
- n_heads: Optional[int] = None,
570
- max_seq_len: int = 512,
571
- attn_dropout: float = 0.0,
572
- res_h1_dropout: float = 0.1,
573
- v1_only: bool = False,
574
- compass_spiral: bool = False,
575
- fast_walk: bool = False,
576
- router_v6: bool = False,
577
- hash_routing: bool = False,
578
- no_walk: bool = False,
579
- walk_to_center: bool = False,
580
- k_core: int = DEFAULT_K_CORE,
581
- center_mix: float = DEFAULT_CENTER_MIX,
582
- sector_mix: bool = False,
583
- sector_frac: float = DEFAULT_SECTOR_FRAC,
584
- sheet_mix: bool = False,
585
- sheet_m: int = DEFAULT_SHEET_M,
586
- sheet_entry_only: bool = False,
587
- edge_neighbor_mix: bool = False,
588
- edge_nb_tau: float = DEFAULT_EDGE_NB_TAU,
589
- pyramid_center: bool = False,
590
- pyramid_narrow: Optional[int] = None,
591
- pyramid_hidden: Optional[int] = None,
592
- hex_pyramid: bool = False,
593
- shared_edges_verts: bool = False,
594
- multi_slot_v1: bool = False,
595
- d_emb_v23: Optional[int] = None,
596
- inter_stage_attn: bool = False,
597
- mask_pad_stages: bool = False,
598
- fuse_mask_pad: bool = False,
599
- zipf_hash_path: Optional[Union[str, Path]] = None,
600
- pad_id_v2: int = 3,
601
- unk_id_v2: int = 4,
602
- pad_id_v3: int = 3,
603
- unk_id_v3: int = 4,
604
- ):
605
- super().__init__()
606
- self.compass_spiral = bool(compass_spiral)
607
- # walk_token vettorizzato (fast_walk.py): stessi risultati del loop per faccia, senza sync per faccia
608
- self.fast_walk = bool(fast_walk)
609
- self.mesh: GeodesicMesh = build_geodesic_mesh(
610
- frequency, compass_spiral=self.compass_spiral
611
- )
612
- self.walk = walk or WalkConfig()
613
- self.d_model = int(d_model)
614
- self.vocab_pezzi = int(vocab_pezzi)
615
- self.vocab_parole = int(vocab_parole)
616
- self.vocab_unioni = int(vocab_unioni)
617
- self.v1_only = bool(v1_only)
618
- self.n_attn_layers = int(n_attn_layers)
619
- self.max_seq_len = int(max_seq_len)
620
- self.d_hidden = int(d_hidden) if d_hidden is not None else None
621
- self.d_emb_v23 = int(d_emb_v23) if d_emb_v23 is not None else None
622
- self.inter_stage_attn = bool(inter_stage_attn)
623
- self.mask_pad_stages = bool(mask_pad_stages)
624
- # v8.1: azzera anche l'addendo emb in fuse su pad/unk (0 nuovi param).
625
- # Richiede mask_pad_stages; se True senza mask_pad viene ignorato in forward.
626
- self.fuse_mask_pad = bool(fuse_mask_pad) and bool(mask_pad_stages)
627
- self.pad_id_v2 = int(pad_id_v2)
628
- self.unk_id_v2 = int(unk_id_v2)
629
- self.pad_id_v3 = int(pad_id_v3)
630
- self.unk_id_v3 = int(unk_id_v3)
631
- heads = int(n_heads) if n_heads is not None else _default_n_heads(d_model)
632
- # attn backbone sempre 4·d; d_hidden slimma solo le FFN delle facce (v8)
633
- attn_ff = 4 * d_model
634
-
635
- self.emb_v1 = nn.Embedding(vocab_pezzi, d_model)
636
- nn.init.normal_(self.emb_v1.weight, mean=0.0, std=0.02)
637
- if not self.v1_only:
638
- if vocab_parole <= 0 or vocab_unioni <= 0:
639
- raise ValueError(
640
- "Cascade 3-stadi richiede vocab_parole > 0 e vocab_unioni > 0 "
641
- "(oppure v1_only=True)"
642
- )
643
- de = self.d_emb_v23 if self.d_emb_v23 is not None else d_model
644
- self.emb_v2 = _make_emb(vocab_parole, de, d_model)
645
- self.emb_v3 = _make_emb(vocab_unioni, de, d_model)
646
-
647
- self.pos_emb = nn.Embedding(self.max_seq_len, d_model)
648
- nn.init.normal_(self.pos_emb.weight, mean=0.0, std=0.02)
649
- self.emb_scale = d_model**-0.5
650
-
651
- self.attn_blocks = nn.ModuleList(
652
- [
653
- CausalAttnBlock(d_model, heads, d_ff=attn_ff, dropout=attn_dropout)
654
- for _ in range(self.n_attn_layers)
655
- ]
656
- )
657
- self.attn_inter2: Optional[CausalAttnBlock] = None
658
- if self.inter_stage_attn and not self.v1_only:
659
- self.attn_inter2 = CausalAttnBlock(d_model, heads, d_ff=attn_ff, dropout=attn_dropout)
660
-
661
- # Banca mesh V1 (80 triangoli a freq=2); stadi 2/3 creati solo se non v1_only
662
- self.to_sphere1 = nn.Linear(d_model, 3)
663
- self.bank1 = PairedFaceBank(self.mesh, d_model, d_hidden)
664
- if not self.v1_only:
665
- self.to_sphere2 = nn.Linear(d_model, 3)
666
- self.to_sphere3 = nn.Linear(d_model, 3)
667
- self.bank2 = PairedFaceBank(self.mesh, d_model, d_hidden)
668
- self.bank3 = PairedFaceBank(self.mesh, d_model, d_hidden)
669
-
670
- # Fuse bilanciato: LN per-addendo (niente emb_scale su V2/V3 — spegneva gli emb)
671
- self.proj_12 = nn.Linear(d_model, d_model)
672
- self.proj_23 = nn.Linear(d_model, d_model)
673
- self.fuse_ln_h2 = nn.LayerNorm(d_model)
674
- self.fuse_ln_e2 = nn.LayerNorm(d_model)
675
- self.fuse_ln_h3 = nn.LayerNorm(d_model)
676
- self.fuse_ln_e3 = nn.LayerNorm(d_model)
677
- self.fuse_norm2 = nn.LayerNorm(d_model)
678
- self.fuse_norm3 = nn.LayerNorm(d_model)
679
-
680
- # Residual bilanciato: LN per-stadio così ||h1||~180 non schiaccia V2/V3
681
- # Dropout 0.1 sulla scorciatoia res_ln_h1 per non monopolizzare lm_head
682
- self.res_h1_dropout = 0.0 if self.v1_only else float(res_h1_dropout)
683
- self.res_ln_h1 = nn.LayerNorm(d_model)
684
- self.res_drop_h1 = nn.Dropout(self.res_h1_dropout)
685
- if not self.v1_only:
686
- self.res_ln_h2 = nn.LayerNorm(d_model)
687
- self.res_ln_h3 = nn.LayerNorm(d_model)
688
- self.final_norm = nn.LayerNorm(d_model)
689
- self.lm_head = nn.Linear(d_model, vocab_pezzi, bias=True)
690
- # self.lm_head.weight separato da emb_v1.weight per rotazione ortogonale Q_lic Watchman Keystone
691
-
692
- self.register_buffer(
693
- "centroids",
694
- torch.from_numpy(self.mesh.face_centroids.astype("float32")),
695
- )
696
- self.register_buffer(
697
- "adjacency",
698
- torch.from_numpy(self.mesh.adjacency.astype("int64")),
699
- )
700
-
701
- # v6 dopo tutti i moduli: stessi valori iniziali di v5 per i parametri comuni a parità di seed
702
- self.hash_routing = bool(hash_routing)
703
- self.router_v6 = bool(router_v6) or self.hash_routing
704
- self.no_walk = bool(no_walk)
705
- self.sheet_mix = bool(sheet_mix)
706
- self.sheet_m = int(sheet_m)
707
- # v19: hop≥1 solo entry; False = storico v13–v18 (mean Δ su fogli cross-face)
708
- self.sheet_entry_only = bool(sheet_entry_only) and self.sheet_mix
709
- # v13 riusa il path sector_mean con buffer (F, 3m) al posto dello spicchio
710
- self.sector_mix = bool(sector_mix) or self.sheet_mix
711
- self.sector_frac = float(sector_frac)
712
- self.edge_neighbor_mix = bool(edge_neighbor_mix)
713
- self.edge_nb_tau = float(edge_nb_tau)
714
- # v11 sector-mix / v12 edge-nb / v13 sheet-mix implicano Spoke-to-Hub
715
- self.walk_to_center = (
716
- bool(walk_to_center) or self.sector_mix or self.edge_neighbor_mix
717
- )
718
- self.k_core = int(k_core)
719
- self.center_mix = float(center_mix)
720
- if self.sheet_mix and self.edge_neighbor_mix:
721
- raise ValueError("sheet_mix (v13) e edge_neighbor_mix (v12) sono mutuamente esclusivi")
722
- if self.sector_mix and self.edge_neighbor_mix:
723
- raise ValueError("sector_mix (v11) e edge_neighbor_mix (v12) sono mutuamente esclusivi")
724
- if self.sheet_mix and self.sheet_m < 1:
725
- raise ValueError("sheet_m deve essere >= 1")
726
- if bool(sheet_entry_only) and not self.sheet_mix:
727
- raise ValueError("sheet_entry_only richiede sheet_mix")
728
- if self.no_walk and self.walk_to_center:
729
- raise ValueError("no_walk e walk_to_center (Spoke-to-Hub) sono mutuamente esclusivi")
730
- if self.no_walk and (not self.router_v6 or int(self.walk.max_hops) != 1):
731
- raise ValueError("no_walk: servono router_v6 o hash_routing e WalkConfig(max_hops=1)")
732
- self.spoke_gates: Optional[nn.ModuleList] = None
733
- if self.walk_to_center:
734
- if not self.hash_routing:
735
- raise ValueError("walk_to_center/Spoke-to-Hub: serve hash_routing (ingresso Zipf)")
736
- hops = int(self.walk.max_hops)
737
- if hops < 2 or hops > 3:
738
- raise ValueError("walk_to_center/Spoke-to-Hub: H_max deve essere 2 o 3")
739
- if not (0.0 <= self.center_mix <= 1.0):
740
- raise ValueError("center_mix deve essere in [0, 1]")
741
- if self.sector_mix and not self.sheet_mix and not (0.0 < self.sector_frac <= 1.0):
742
- raise ValueError("sector_frac deve essere in (0, 1]")
743
- parent, is_hub, hub_ids = build_spoke_hub_parent(
744
- self.mesh.adjacency, self.mesh.face_centroids, k_core=self.k_core
745
- )
746
- self.register_buffer("parent_face", torch.from_numpy(parent.astype("int64")))
747
- self.register_buffer("is_hub", torch.from_numpy(is_hub.astype("bool")))
748
- self.register_buffer("hub_ids", torch.from_numpy(hub_ids.astype("int64")))
749
- if self.sheet_mix and self.sheet_entry_only:
750
- # v19: niente buffer fogli — hop≥1 tocca solo l'entry (vedi walk)
751
- self.sector_faces = None # type: ignore[assignment]
752
- self.sector_k = 1
753
- elif self.sheet_mix:
754
- sheets = build_complementary_sheets(
755
- self.mesh.face_centroids,
756
- spiral_rank=getattr(self.mesh, "spiral_rank", None),
757
- m_per_sheet=self.sheet_m,
758
- )
759
- self.register_buffer(
760
- "sector_faces", torch.from_numpy(sheets.astype("int64"))
761
- )
762
- self.sector_k = int(sheets.shape[1])
763
- elif self.sector_mix:
764
- sectors = build_azimuth_sectors(
765
- self.mesh.face_centroids, sector_frac=self.sector_frac
766
- )
767
- self.register_buffer(
768
- "sector_faces", torch.from_numpy(sectors.astype("int64"))
769
- )
770
- self.sector_k = int(sectors.shape[1])
771
- else:
772
- self.sector_faces = None # type: ignore[assignment]
773
- self.sector_k = 0
774
- if self.edge_neighbor_mix:
775
- w = build_edge_neighbor_weights(
776
- self.mesh.adjacency, self.mesh.face_centroids, tau=self.edge_nb_tau
777
- )
778
- self.register_buffer("edge_nb_w", torch.from_numpy(w))
779
- else:
780
- self.edge_nb_w = None # type: ignore[assignment]
781
- self.spoke_res_scale = 1.0 / math.sqrt(float(hops))
782
- # gate scalare hop≥1; bias=0 → σ≈0.5 a init (Argon)
783
- n_banks = 1 if self.v1_only else 3
784
- gates = []
785
- for _ in range(n_banks):
786
- lin = nn.Linear(self.d_model, 1)
787
- nn.init.zeros_(lin.weight)
788
- nn.init.zeros_(lin.bias)
789
- gates.append(lin)
790
- self.spoke_gates = nn.ModuleList(gates)
791
- else:
792
- self.parent_face = None # type: ignore[assignment]
793
- self.is_hub = None # type: ignore[assignment]
794
- self.hub_ids = None # type: ignore[assignment]
795
- self.sector_faces = None # type: ignore[assignment]
796
- self.sector_k = 0
797
- self.sector_mix = False
798
- self.sheet_mix = False
799
- self.sheet_entry_only = False
800
- self.edge_neighbor_mix = False
801
- self.edge_nb_w = None # type: ignore[assignment]
802
- self.spoke_res_scale = 1.0
803
- self.center_mix = 0.0
804
- self.v6_logit_scale_max = V6_LOGIT_SCALE_MAX
805
- self.v6_bias_rate = V6_BIAS_RATE
806
- if self.router_v6:
807
- n_faces = int(self.mesh.num_faces)
808
- sizes = (self.vocab_pezzi, self.vocab_parole, self.vocab_unioni)
809
- for s, bank in enumerate(self._banks(), start=1):
810
- if self.hash_routing:
811
- bank.register_buffer("hash_face", hash_faces(sizes[s - 1], n_faces, s))
812
- delattr(self, f"to_sphere{s}")
813
- setattr(self, f"to_sphere{s}", None)
814
- else:
815
- bank.logit_scale = nn.Parameter(torch.tensor(V6_LOGIT_SCALE_INIT))
816
- bank.register_buffer("face_bias", torch.zeros(n_faces))
817
-
818
- self.multi_slot_v1 = bool(multi_slot_v1)
819
- if zipf_hash_path is not None:
820
- if not self.hash_routing:
821
- raise ValueError("zipf_hash_path richiede hash_routing")
822
- self.load_zipf_hash(zipf_hash_path)
823
- elif self.multi_slot_v1:
824
- # Eval senza zipf path: buffer placeholder (V, K=3) riempito da state_dict
825
- v = int(self.bank1.hash_face.numel())
826
- self.bank1.register_buffer(
827
- "hash_slots",
828
- self.bank1.hash_face.unsqueeze(1).expand(v, 3).contiguous().clone(),
829
- )
830
-
831
- # piramidi e centri per ultimi: i parametri della base hanno gli stessi valori iniziali a parità di seed
832
- self.hex_pyramid = bool(hex_pyramid)
833
- self.shared_edges_verts = bool(shared_edges_verts)
834
- if self.hex_pyramid and self.shared_edges_verts:
835
- raise ValueError("hex_pyramid e shared_edges_verts (v12) sono mutuamente esclusivi")
836
- self.pyramid_center = bool(pyramid_center) or self.hex_pyramid or self.shared_edges_verts
837
- self.pyramids: Optional[nn.ModuleList] = None
838
- if self.pyramid_center:
839
- if not self.hash_routing:
840
- raise ValueError("pyramid_center/hex_pyramid: serve hash_routing (la piramide è quella della faccia d'ingresso)")
841
- self.pyramid_narrow = int(pyramid_narrow or self.d_model // 2)
842
- self.pyramid_hidden = int(pyramid_hidden or 2 * self.d_model)
843
- # generatore proprio e stato globale ripristinato: su CPU anche le maschere di dropout restano quelle della base
844
- gen = torch.Generator().manual_seed((torch.initial_seed() + PYRAMID_SEED_OFFSET) % 2**63)
845
- rng = torch.get_rng_state()
846
- if self.hex_pyramid:
847
- face_verts = torch.from_numpy(self.mesh.faces.astype("int64"))
848
- n_vert = int(self.mesh.num_vertices)
849
- self.pyramids = nn.ModuleList(
850
- HexPyramidCenter(
851
- self.d_model, n_vert, face_verts, self.pyramid_narrow, self.pyramid_hidden, gen
852
- )
853
- for _ in self._banks()
854
- )
855
- elif self.shared_edges_verts:
856
- face_edges_np, n_edges = build_face_edge_ids(self.mesh.faces)
857
- face_edges = torch.from_numpy(face_edges_np.astype("int64"))
858
- face_verts = torch.from_numpy(self.mesh.faces.astype("int64"))
859
- n_vert = int(self.mesh.num_vertices)
860
- self.pyramids = nn.ModuleList(
861
- SharedEdgeVertexPyramid(
862
- self.d_model,
863
- int(self.mesh.num_faces),
864
- int(n_edges),
865
- n_vert,
866
- face_edges,
867
- face_verts,
868
- self.pyramid_narrow,
869
- self.pyramid_hidden,
870
- gen,
871
- )
872
- for _ in self._banks()
873
- )
874
- else:
875
- self.pyramids = nn.ModuleList(
876
- PyramidCenter(self.d_model, int(self.mesh.num_faces), self.pyramid_narrow, self.pyramid_hidden, gen)
877
- for _ in self._banks()
878
- )
879
- torch.set_rng_state(rng)
880
-
881
- from watchman_keystone_r08 import NonLinearWatchmanKeystoneR08, OFFICIAL_SENTINEL_LICENSE_SUMMARY
882
- self.watchman = NonLinearWatchmanKeystoneR08(d_model=self.d_model)
883
- self._watchman_is_ethical = True
884
- self._watchman_license_text = OFFICIAL_SENTINEL_LICENSE_SUMMARY
885
-
886
- def _banks(self) -> List[PairedFaceBank]:
887
- return [self.bank1] if self.v1_only else [self.bank1, self.bank2, self.bank3]
888
-
889
- def load_zipf_hash(self, path: Union[str, Path]) -> None:
890
- """Copia ``hash_face_{1,2,3}`` (e opz. ``hash_slots_1`` multi-slot V1) da un ``.pt`` Zipf."""
891
- path = Path(path)
892
- try:
893
- blob = torch.load(path, map_location="cpu", weights_only=False)
894
- except TypeError:
895
- blob = torch.load(path, map_location="cpu")
896
- n_faces = int(self.mesh.num_faces)
897
- for s, bank in enumerate(self._banks(), start=1):
898
- key = f"hash_face_{s}"
899
- if key not in blob:
900
- raise KeyError(f"{path}: manca {key}")
901
- t = blob[key].long().view(-1)
902
- if t.numel() != bank.hash_face.numel():
903
- raise ValueError(
904
- f"{path} {key}: len={t.numel()} ≠ bank.hash_face={bank.hash_face.numel()}"
905
- )
906
- if int(t.max()) >= n_faces or int(t.min()) < 0:
907
- raise ValueError(f"{path} {key}: facce fuori da [0, {n_faces})")
908
- bank.hash_face.copy_(t)
909
- # Multi-slot solo stadio 1 (pezzi): (V, K) con K copie della primaria se non top-N
910
- if "hash_slots_1" in blob:
911
- slots = blob["hash_slots_1"].long()
912
- if slots.ndim != 2 or slots.shape[0] != self.bank1.hash_face.numel():
913
- raise ValueError(
914
- f"{path} hash_slots_1: shape={tuple(slots.shape)} "
915
- f"atteso (V={self.bank1.hash_face.numel()}, K)"
916
- )
917
- if int(slots.max()) >= n_faces or int(slots.min()) < 0:
918
- raise ValueError(f"{path} hash_slots_1: facce fuori da [0, {n_faces})")
919
- self.bank1.register_buffer("hash_slots", slots.contiguous())
920
- self.multi_slot_v1 = True
921
- elif self.multi_slot_v1:
922
- raise ValueError(f"{path}: multi_slot_v1 richiesto ma manca hash_slots_1")
923
-
924
- def _active_mask(self, stage: int, ids: torch.Tensor) -> Optional[torch.Tensor]:
925
- """Maschera token attivi per stadi 2/3 con ``mask_pad_stages`` (pad/unk → False)."""
926
- if not self.mask_pad_stages or stage < 1:
927
- return None
928
- if stage == 1:
929
- pad, unk = self.pad_id_v2, self.unk_id_v2
930
- elif stage == 2:
931
- pad, unk = self.pad_id_v3, self.unk_id_v3
932
- else:
933
- return None
934
- return (ids != pad) & (ids != unk)
935
-
936
- def _v6_entry(
937
- self,
938
- h: torch.Tensor,
939
- bank: PairedFaceBank,
940
- to_sphere: Optional[nn.Linear],
941
- entry_ids: Optional[torch.Tensor],
942
- ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
943
- """Faccia d'ingresso v6 (B,) e peso straight-through w (B, 1), None senza gradiente.
944
-
945
- w = p_f / sg(p_f) è scritto come exp(log p_f − sg(log p_f)): il bias può
946
- scegliere una faccia con p_f sotto il minimo di float32 e il rapporto
947
- diventerebbe 0/0. In training il carico d'ingresso per faccia si accumula
948
- sul device per ``update_face_bias``.
949
- """
950
- if self.hash_routing:
951
- if entry_ids is None:
952
- raise ValueError("hash_routing: servono gli id dei token per la faccia d'ingresso")
953
- # v9 multi-slot V1: in training campiona tra K facce; in eval usa lo slot 0 (primaria)
954
- slots = getattr(bank, "hash_slots", None)
955
- if slots is not None:
956
- row = slots[entry_ids] # (..., K)
957
- if self.training:
958
- k = int(row.shape[-1])
959
- idx = torch.randint(0, k, entry_ids.shape, device=entry_ids.device)
960
- face = row.gather(-1, idx.unsqueeze(-1)).squeeze(-1)
961
- else:
962
- face = row[..., 0]
963
- return face, None
964
- return bank.hash_face[entry_ids], None
965
- scores = F.normalize(to_sphere(h), dim=-1) @ self.centroids.T # (B, F)
966
- face = (scores + bank.face_bias).argmax(dim=-1)
967
- if self.training:
968
- n_faces = scores.shape[1]
969
- load = (face.unsqueeze(1) == torch.arange(n_faces, device=face.device)).sum(0)
970
- prev = bank.__dict__.get("_v6_load")
971
- bank.__dict__["_v6_load"] = load if prev is None else prev + load
972
- if not torch.is_grad_enabled():
973
- return face, None
974
- scale = bank.logit_scale.clamp(max=self.v6_logit_scale_max).exp()
975
- lp = F.log_softmax(scores * scale, dim=-1).gather(1, face.unsqueeze(1))
976
- return face, torch.exp(lp - lp.detach())
977
-
978
- def _v6_route(
979
- self, face: torch.Tensor, logits: torch.Tensor
980
- ) -> Tuple[torch.Tensor, torch.Tensor, Optional[torch.Tensor]]:
981
- """Porte come ``_route_step``, uscita dura a soglia; ritorna anche il peso del salto successivo.
982
-
983
- w = w_porta · p_stay / sg(p_stay) (B, 1), None senza gradiente: w_porta è il
984
- valore Gumbel-ST della porta scelta (solo in training), p_stay =
985
- 1 − probs[:, 3] = exp(LSE(z[:3]) − LSE(z)). Sui token che proseguono il
986
- logit di uscita riceve d log p_stay / d z_uscita = −p_exit volte il
987
- gradiente del peso dell'esperto del salto successivo.
988
- Con ``no_walk`` escono tutti: porte e uscita non vengono valutate.
989
- Con ``walk_to_center`` (Spoke-to-Hub) il routing è in ``_walk_token_spoke_hub``:
990
- qui si usa solo ``parent_face`` (niente Gumbel).
991
- """
992
- if self.no_walk:
993
- return face, torch.ones_like(face, dtype=torch.bool), None
994
- if self.walk_to_center:
995
- nxt = self.parent_face[face.long()]
996
- exit_mask = nxt == face
997
- return nxt, exit_mask, None
998
- temp = max(self.walk.router_temperature, 1e-6)
999
- z = logits / temp
1000
- probs = F.softmax(z, dim=-1)
1001
- exit_mask = probs[:, 3] >= self.walk.exit_threshold
1002
- neighbors = self.adjacency[face.long()]
1003
- w_side: Optional[torch.Tensor] = None
1004
- if self._hard_routing():
1005
- side = probs[:, :3].argmax(dim=-1)
1006
- else:
1007
- side_oh = F.gumbel_softmax(logits[:, :3] / temp, hard=True, dim=-1)
1008
- side = side_oh.argmax(dim=-1)
1009
- w_side = side_oh.gather(1, side.unsqueeze(1))
1010
- next_face = neighbors.gather(1, side.unsqueeze(1)).squeeze(1)
1011
- if not torch.is_grad_enabled():
1012
- return next_face, exit_mask, None
1013
- log_stay = torch.logsumexp(z[:, :3], dim=-1, keepdim=True) - torch.logsumexp(z, dim=-1, keepdim=True)
1014
- w = torch.exp(log_stay - log_stay.detach())
1015
- return next_face, exit_mask, (w if w_side is None else w_side * w)
1016
-
1017
- def _walk_token_v6_loop(
1018
- self,
1019
- h: torch.Tensor,
1020
- bank: PairedFaceBank,
1021
- to_sphere: Optional[nn.Linear],
1022
- entry_ids: Optional[torch.Tensor] = None,
1023
- record_path: bool = False,
1024
- active: Optional[torch.Tensor] = None,
1025
- ) -> Tuple[torch.Tensor, Optional[List[List[int]]]]:
1026
- """Cammino v6 col loop per faccia: riferimento di ``fast_walk_token_v6`` nei test."""
1027
- b = h.shape[0]
1028
- face, w = self._v6_entry(h, bank, to_sphere, entry_ids)
1029
- if active is None:
1030
- active = torch.ones(b, dtype=torch.bool, device=h.device)
1031
- else:
1032
- active = active.to(device=h.device, dtype=torch.bool)
1033
- face0 = torch.where(active, face, torch.full_like(face, -1))
1034
- paths = [[int(f)] for f in face0.tolist()] if record_path else None
1035
- for _ in range(self.walk.max_hops):
1036
- if not bool(active.any()):
1037
- break
1038
- logits = h.new_zeros(b, 4)
1039
- h_new = h.clone()
1040
- for f_id in face[active].unique().tolist():
1041
- mask = active & (face == f_id)
1042
- blk = bank.blocks[int(bank.face_to_slot[f_id].item())]
1043
- x = h[mask]
1044
- delta = blk.ff(blk.norm(x))
1045
- out = x + delta if w is None else x + w[mask] * delta
1046
- h_new[mask] = out
1047
- logits[mask] = blk.router(out)
1048
- h = h_new
1049
- next_face, exit_mask, w = self._v6_route(face, logits)
1050
- moved = active & ~exit_mask
1051
- face = torch.where(moved, next_face, face)
1052
- if paths is not None:
1053
- for i in range(b):
1054
- if moved[i]:
1055
- paths[i].append(int(face[i].item()))
1056
- elif active[i] and exit_mask[i]:
1057
- paths[i].append(-1)
1058
- active = active & ~exit_mask
1059
- return h, paths
1060
-
1061
- @torch.no_grad()
1062
- def update_face_bias(self) -> None:
1063
- """Loss-Free Balancing: b_f += u · sign(carico medio − carico_f) col carico d'ingresso accumulato."""
1064
- if not self.router_v6 or self.hash_routing:
1065
- return
1066
- for bank in self._banks():
1067
- load = bank.__dict__.pop("_v6_load", None)
1068
- if load is None:
1069
- continue
1070
- load = load.to(bank.face_bias.dtype)
1071
- bank.face_bias.add_(torch.sign(load.mean() - load), alpha=self.v6_bias_rate)
1072
-
1073
- def _hard_routing(self) -> bool:
1074
- """Hard (argmax) in eval o se WalkConfig.hard_routing; soft/ST in train."""
1075
- return bool(self.walk.hard_routing) or (not self.training)
1076
-
1077
- def project_to_face(
1078
- self, h: torch.Tensor, to_sphere: nn.Linear
1079
- ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
1080
- """
1081
- Proietta h sulla sfera �� faccia.
1082
- Train (soft): Gumbel-ST one-hot (B, F) così i grad arrivano a to_sphere.
1083
- Eval/hard: solo face id (argmax), face_oh=None.
1084
- """
1085
- direction = F.normalize(to_sphere(h), dim=-1)
1086
- scores = direction @ self.centroids.T # (B, F)
1087
- if self._hard_routing():
1088
- return scores.argmax(dim=-1), None
1089
- temp = max(self.walk.router_temperature, 1e-6)
1090
- face_oh = F.gumbel_softmax(scores / temp, hard=True, dim=-1) # ST soft→hard
1091
- face = face_oh.argmax(dim=-1) # solo indice forward; massa ST resta in face_oh
1092
- return face, face_oh
1093
-
1094
- def _route_step(
1095
- self, face: torch.Tensor, logits: torch.Tensor
1096
- ) -> Tuple[torch.Tensor, torch.Tensor, Optional[torch.Tensor]]:
1097
- """
1098
- Router ternario + exit.
1099
- Train (soft): Gumbel-ST su 3 lati (niente argmax sulla one-hot) + mixture
1100
- su adjacency → side_oh per propagare i grad al router.
1101
- Eval/hard: argmax discreto, side_oh=None.
1102
- """
1103
- temp = max(self.walk.router_temperature, 1e-6)
1104
- probs = F.softmax(logits / temp, dim=-1)
1105
- exit_prob = probs[:, 3]
1106
- neighbors = self.adjacency[face.long()] # (B, 3)
1107
- exit_mask = exit_prob >= self.walk.exit_threshold
1108
-
1109
- if self._hard_routing():
1110
- side = probs[:, :3].argmax(dim=-1)
1111
- next_face = neighbors.gather(1, side.unsqueeze(1)).squeeze(1)
1112
- return next_face, exit_mask, None
1113
-
1114
- # Gumbel-ST: forward one-hot, backward softmax — non chiamare .argmax sulla ST
1115
- side_oh = F.gumbel_softmax(logits[:, :3] / temp, hard=True, dim=-1) # (B, 3)
1116
- side = side_oh.argmax(dim=-1)
1117
- next_face = neighbors.gather(1, side.unsqueeze(1)).squeeze(1)
1118
- return next_face, exit_mask, side_oh
1119
-
1120
- def _scatter_side_oh(
1121
- self, face: torch.Tensor, side_oh: torch.Tensor
1122
- ) -> torch.Tensor:
1123
- """Soft/ST mixture su adjacency: massa dei 3 lati → one-hot sparsa su F facce."""
1124
- b = face.shape[0]
1125
- n_faces = int(self.centroids.shape[0])
1126
- neighbors = self.adjacency[face.long()] # (B, 3)
1127
- face_oh = side_oh.new_zeros(b, n_faces)
1128
- face_oh.scatter_add_(1, neighbors, side_oh)
1129
- return face_oh
1130
-
1131
- def _spoke_stage_index(self, bank: PairedFaceBank) -> int:
1132
- for i, b in enumerate(self._banks()):
1133
- if b is bank:
1134
- return i
1135
- raise KeyError("bank non appartiene al modello")
1136
-
1137
- def _apply_sector_mean(
1138
- self,
1139
- h: torch.Tensor,
1140
- bank: PairedFaceBank,
1141
- entry: torch.Tensor,
1142
- active: torch.Tensor,
1143
- g: Optional[torch.Tensor],
1144
- st: Optional[dict] = None,
1145
- ) -> torch.Tensor:
1146
- """v11: ``x ← x + g · mean_{f∈settore(entry)}(Δ_f)`` (hop≥1).
1147
-
1148
- Settore fissato sulla faccia d'ingresso Zipf. ``g=None`` → peso 1.0.
1149
- """
1150
- assert self.sector_faces is not None
1151
- k = int(self.sector_k)
1152
- if k <= 0:
1153
- return h
1154
- use_fast = st is not None
1155
- delta_sum: Optional[torch.Tensor] = None
1156
- for slot in range(k):
1157
- face_k = self.sector_faces[entry.long(), slot]
1158
- if use_fast:
1159
- assert st is not None
1160
- out, _lg, counts = grouped_block_forward(
1161
- st, h, face_k, active, gate=None
1162
- )
1163
- if st["visited"] is not None:
1164
- st["visited"].add(counts) # type: ignore[union-attr]
1165
- # out = x+Δ sulle attive; sulle inattive out è spazzatura → maschera dopo
1166
- d = out - h
1167
- else:
1168
- d = h.new_zeros(h.shape)
1169
- for f_id in face_k[active].unique().tolist():
1170
- mask = active & (face_k == f_id)
1171
- blk = bank.blocks[int(bank.face_to_slot[f_id].item())]
1172
- x = h[mask]
1173
- d[mask] = blk.ff(blk.norm(x))
1174
- delta_sum = d if delta_sum is None else delta_sum + d
1175
- assert delta_sum is not None
1176
- mean_d = delta_sum / float(k)
1177
- if g is None:
1178
- upd = h + mean_d
1179
- else:
1180
- upd = h + g * mean_d
1181
- return torch.where(active.unsqueeze(-1), upd, h)
1182
-
1183
- def _apply_edge_neighbor_mix(
1184
- self,
1185
- h: torch.Tensor,
1186
- bank: PairedFaceBank,
1187
- entry: torch.Tensor,
1188
- active: torch.Tensor,
1189
- g: Optional[torch.Tensor],
1190
- st: Optional[dict] = None,
1191
- ) -> torch.Tensor:
1192
- """v12: ``x ← x + g · Σ_k w_{e,k} Δ_{nb_k}`` sui 3 lati di ``entry`` (pesi verso polo)."""
1193
- assert self.edge_nb_w is not None
1194
- e = entry.long()
1195
- nbs = self.adjacency[e] # (B, 3)
1196
- w = self.edge_nb_w[e] # (B, 3)
1197
- use_fast = st is not None
1198
- weighted: Optional[torch.Tensor] = None
1199
- for slot in range(3):
1200
- face_k = nbs[:, slot]
1201
- wk = w[:, slot].unsqueeze(-1)
1202
- if use_fast:
1203
- assert st is not None
1204
- out, _lg, counts = grouped_block_forward(
1205
- st, h, face_k, active, gate=None
1206
- )
1207
- if st["visited"] is not None:
1208
- st["visited"].add(counts) # type: ignore[union-attr]
1209
- d = out - h
1210
- else:
1211
- d = h.new_zeros(h.shape)
1212
- for f_id in face_k[active].unique().tolist():
1213
- mask = active & (face_k == f_id)
1214
- blk = bank.blocks[int(bank.face_to_slot[f_id].item())]
1215
- x = h[mask]
1216
- d[mask] = blk.ff(blk.norm(x))
1217
- term = wk * d
1218
- weighted = term if weighted is None else weighted + term
1219
- assert weighted is not None
1220
- if g is None:
1221
- upd = h + weighted
1222
- else:
1223
- upd = h + g * weighted
1224
- return torch.where(active.unsqueeze(-1), upd, h)
1225
-
1226
- def _walk_token_spoke_hub(
1227
- self,
1228
- h: torch.Tensor,
1229
- bank: PairedFaceBank,
1230
- to_sphere: Optional[nn.Linear],
1231
- entry_ids: Optional[torch.Tensor] = None,
1232
- record_path: bool = False,
1233
- active: Optional[torch.Tensor] = None,
1234
- ) -> Tuple[torch.Tensor, Optional[List[List[int]]]]:
1235
- """v10 Spoke-to-Hub / v11 sector-mix / v12 edge-nb / v13–v19 sheet: parent→hub; no Gumbel.
1236
-
1237
- hop0: solo faccia entry, residuo pieno.
1238
- hop≥1: v10 = blocco ``face`` corrente con ``g=(c+(1-c)σ)/√H``;
1239
- v11 (``sector_mix``) = media Δ sul settore azimutale dell'ingresso;
1240
- v12 (``edge_neighbor_mix``) = mix pesato sui 3 vicini di lato (anche al polo);
1241
- v13–v18 (``sheet_mix``, default) = media Δ sui fogli complementari dell'ingresso;
1242
- v19 (``sheet_entry_only``) = di nuovo solo blocco entry (niente cross-face).
1243
- pad/unk (``active=False``): 0 hop. Train==eval.
1244
- """
1245
- assert self.spoke_gates is not None and self.parent_face is not None
1246
- b = h.shape[0]
1247
- face, _ = self._v6_entry(h, bank, to_sphere, entry_ids)
1248
- if active is None:
1249
- active = torch.ones(b, dtype=torch.bool, device=h.device)
1250
- else:
1251
- active = active.to(device=h.device, dtype=torch.bool)
1252
- entry = face.clone()
1253
- face0 = torch.where(active, face, torch.full_like(face, -1))
1254
- max_hops = int(self.walk.max_hops)
1255
- if b == 0 or max_hops <= 0:
1256
- return h, (_build_paths(face0, []) if record_path else None)
1257
-
1258
- gate_lin = self.spoke_gates[self._spoke_stage_index(bank)]
1259
- scale = float(self.spoke_res_scale)
1260
- codes: List[torch.Tensor] = []
1261
- use_fast = bool(self.fast_walk)
1262
- st = stack_bank(bank) if use_fast else None
1263
- # storico sheet/sector: media su sector_faces; v19 entry-only salta questo path
1264
- use_sector = (
1265
- bool(self.sector_mix)
1266
- and self.sector_faces is not None
1267
- and not bool(self.sheet_entry_only)
1268
- )
1269
- use_sheet_entry = bool(self.sheet_mix) and bool(self.sheet_entry_only)
1270
- use_edge_nb = bool(self.edge_neighbor_mix) and self.edge_nb_w is not None
1271
-
1272
- c_mix = float(self.center_mix)
1273
- for hop in range(max_hops):
1274
- if not bool(active.any()):
1275
- break
1276
- # hop0: residuo pieno (v8.1); hop≥1: g=(c+(1-c)σ)/√H (liscio, no clamp)
1277
- if hop == 0:
1278
- g: Optional[torch.Tensor] = None
1279
- else:
1280
- g = (c_mix + (1.0 - c_mix) * torch.sigmoid(gate_lin(h))) * scale
1281
- if use_edge_nb and hop >= 1:
1282
- h = self._apply_edge_neighbor_mix(h, bank, entry, active, g, st=st)
1283
- elif use_sector and hop >= 1:
1284
- h = self._apply_sector_mean(h, bank, entry, active, g, st=st)
1285
- elif use_sheet_entry and hop >= 1:
1286
- # v19: solo esperto della faccia d'ingresso (fogli non toccano altre facce)
1287
- apply_face = entry
1288
- if use_fast:
1289
- assert st is not None
1290
- out, _lg, counts = grouped_block_forward(
1291
- st, h, apply_face, active, gate=g
1292
- )
1293
- if st["visited"] is not None:
1294
- st["visited"].add(counts) # type: ignore[union-attr]
1295
- h = torch.where(active.unsqueeze(-1), out, h)
1296
- else:
1297
- h_new = h.clone()
1298
- for f_id in apply_face[active].unique().tolist():
1299
- mask = active & (apply_face == f_id)
1300
- blk = bank.blocks[int(bank.face_to_slot[f_id].item())]
1301
- x = h[mask]
1302
- delta = blk.ff(blk.norm(x))
1303
- if g is None:
1304
- h_new[mask] = x + delta
1305
- else:
1306
- h_new[mask] = x + g[mask] * delta
1307
- h = h_new
1308
- elif use_fast:
1309
- assert st is not None
1310
- out, _lg, counts = grouped_block_forward(
1311
- st, h, face, active, gate=g
1312
- )
1313
- if st["visited"] is not None:
1314
- st["visited"].add(counts) # type: ignore[union-attr]
1315
- h = torch.where(active.unsqueeze(-1), out, h)
1316
- else:
1317
- h_new = h.clone()
1318
- for f_id in face[active].unique().tolist():
1319
- mask = active & (face == f_id)
1320
- blk = bank.blocks[int(bank.face_to_slot[f_id].item())]
1321
- x = h[mask]
1322
- delta = blk.ff(blk.norm(x))
1323
- if g is None:
1324
- h_new[mask] = x + delta
1325
- else:
1326
- h_new[mask] = x + g[mask] * delta
1327
- h = h_new
1328
- # v12: nessuno stop anticipato su hub — tutti fanno hop1 sui 3 lati
1329
- if use_edge_nb:
1330
- stop = torch.full(
1331
- (b,), hop == max_hops - 1, device=h.device, dtype=torch.bool
1332
- )
1333
- moved = torch.zeros_like(active)
1334
- # face resta sull'entry; hop1 usa adjacency[entry]
1335
- else:
1336
- nxt = self.parent_face[face.long()]
1337
- at_hub = nxt == face
1338
- stop = at_hub | (hop == max_hops - 1)
1339
- moved = active & ~stop
1340
- face = torch.where(moved, nxt, face)
1341
- just_exit = active & stop
1342
- active = active & ~stop
1343
- if record_path:
1344
- codes.append(
1345
- torch.where(
1346
- moved,
1347
- face,
1348
- torch.where(just_exit, torch.full_like(face, -1), torch.full_like(face, -2)),
1349
- )
1350
- )
1351
-
1352
- return h, (_build_paths(face0, codes) if record_path else None)
1353
-
1354
- def walk_token(
1355
- self,
1356
- h: torch.Tensor,
1357
- bank: PairedFaceBank,
1358
- to_sphere: Optional[nn.Linear],
1359
- record_path: bool = False,
1360
- entry_ids: Optional[torch.Tensor] = None,
1361
- active: Optional[torch.Tensor] = None,
1362
- ) -> Tuple[torch.Tensor, Optional[List[List[int]]]]:
1363
- if self.walk_to_center:
1364
- return self._walk_token_spoke_hub(
1365
- h, bank, to_sphere, entry_ids=entry_ids, record_path=record_path, active=active
1366
- )
1367
- if self.router_v6:
1368
- if self.fast_walk:
1369
- return fast_walk_token_v6(
1370
- self, h, bank, to_sphere, entry_ids=entry_ids, record_path=record_path, active=active
1371
- )
1372
- return self._walk_token_v6_loop(
1373
- h, bank, to_sphere, entry_ids=entry_ids, record_path=record_path, active=active
1374
- )
1375
- if self.fast_walk:
1376
- return fast_walk_token(self, h, bank, to_sphere, record_path=record_path)
1377
- b = h.shape[0]
1378
- face, face_oh = self.project_to_face(h, to_sphere)
1379
- paths: Optional[List[List[int]]] = (
1380
- [[int(face[i].item())] for i in range(b)] if record_path else None
1381
- )
1382
- active = torch.ones(b, dtype=torch.bool, device=h.device)
1383
-
1384
- for _ in range(self.walk.max_hops):
1385
- if not active.any():
1386
- break
1387
- logits = h.new_zeros(b, 4)
1388
- h_new = h.clone()
1389
- for f_id in face[active].unique().tolist():
1390
- mask = active & (face == f_id)
1391
- if not mask.any():
1392
- continue
1393
- out, lg = bank.forward_face(int(f_id), h[mask])
1394
- if face_oh is not None:
1395
- # ST: w≈1 in forward; backward → face_oh → to_sphere / router
1396
- w = face_oh[mask, int(f_id)].unsqueeze(-1)
1397
- h_new[mask] = out * w
1398
- else:
1399
- h_new[mask] = out
1400
- logits[mask] = lg
1401
- h = h_new
1402
- next_face, exit_mask, side_oh = self._route_step(face, logits)
1403
- moved = active & ~exit_mask
1404
- if side_oh is not None:
1405
- # sum(ST one-hot)=1 in forward: ancora i grad al router anche su exit/ultimo hop
1406
- scale = side_oh.sum(dim=-1, keepdim=True)
1407
- h = torch.where(active.unsqueeze(-1), h * scale, h)
1408
- routed_oh = self._scatter_side_oh(face, side_oh)
1409
- if face_oh is None:
1410
- face_oh = routed_oh
1411
- else:
1412
- face_oh = torch.where(moved.unsqueeze(-1), routed_oh, face_oh)
1413
- face = torch.where(moved, next_face, face)
1414
- just_exit = active & exit_mask
1415
- active = active & ~exit_mask
1416
- if record_path and paths is not None:
1417
- for i in range(b):
1418
- if moved[i]:
1419
- paths[i].append(int(face[i].item()))
1420
- elif just_exit[i]:
1421
- paths[i].append(-1)
1422
- return h, paths
1423
-
1424
- def _descend(
1425
- self,
1426
- stage: int,
1427
- h: torch.Tensor,
1428
- ids: torch.Tensor,
1429
- active_bt: Optional[torch.Tensor] = None,
1430
- ) -> torch.Tensor:
1431
- """Piramide della faccia d'ingresso e centro dello stadio (da 0) su h (B, T, d); identità senza piramidi."""
1432
- if self.pyramids is None:
1433
- return h
1434
- bank = self._banks()[stage]
1435
- face = bank.hash_face[ids]
1436
- # Allinea multi-slot V1 alla stessa regola di ``_v6_entry`` (slot 0 in eval; sample in train)
1437
- slots = getattr(bank, "hash_slots", None)
1438
- if slots is not None:
1439
- row = slots[ids]
1440
- if self.training:
1441
- k = int(row.shape[-1])
1442
- idx = torch.randint(0, k, ids.shape, device=ids.device)
1443
- face = row.gather(-1, idx.unsqueeze(-1)).squeeze(-1)
1444
- else:
1445
- face = row[..., 0]
1446
- out = self.pyramids[stage](h, face)
1447
- if active_bt is None:
1448
- return out
1449
- return torch.where(active_bt.unsqueeze(-1), out, h)
1450
-
1451
- def _walk_seq(
1452
- self,
1453
- h: torch.Tensor,
1454
- bank: PairedFaceBank,
1455
- to_sphere: Optional[nn.Linear],
1456
- record_path: bool,
1457
- entry_ids: Optional[torch.Tensor] = None,
1458
- active_bt: Optional[torch.Tensor] = None,
1459
- ) -> Tuple[torch.Tensor, Optional[List[List[List[int]]]]]:
1460
- b, t, d = h.shape
1461
- kw: dict = {}
1462
- if entry_ids is not None:
1463
- kw["entry_ids"] = entry_ids.reshape(b * t)
1464
- if active_bt is not None:
1465
- kw["active"] = active_bt.reshape(b * t)
1466
- flat, paths_flat = self.walk_token(
1467
- h.reshape(b * t, d), bank, to_sphere, record_path=record_path, **kw
1468
- )
1469
- h_out = flat.view(b, t, d)
1470
- paths = None
1471
- if record_path and paths_flat is not None:
1472
- paths = [
1473
- [paths_flat[bi * t + ti] for ti in range(t)] for bi in range(b)
1474
- ]
1475
- return h_out, paths
1476
-
1477
- def forward(
1478
- self,
1479
- pezzi_ids: torch.Tensor,
1480
- parole_ids: Optional[torch.Tensor] = None,
1481
- unioni_ids: Optional[torch.Tensor] = None,
1482
- record_path: bool = False,
1483
- ) -> Tuple[torch.Tensor, Optional[List[List[List[int]]]]]:
1484
- """
1485
- pezzi/parole/unioni: (B, T) allineati sulla timeline pezzi.
1486
- Se self.v1_only: richiede solo pezzi_ids (parole_ids/unioni_ids ignorati/None).
1487
- returns: logits (B, T, V_pezzi), paths opzionale dell'ultimo mesh walk.
1488
- """
1489
- b, t = pezzi_ids.shape
1490
- if t > self.max_seq_len:
1491
- raise ValueError(f"seq_len={t} > max_seq_len={self.max_seq_len}")
1492
-
1493
- pos = torch.arange(t, device=pezzi_ids.device).unsqueeze(0)
1494
- h = self.emb_v1(pezzi_ids) * self.emb_scale + self.pos_emb(pos)
1495
-
1496
- # Dei CausalAttnBlock: 1 su V1 (prima di bank1), 1 dopo
1497
- for block in self.attn_blocks[:-1]:
1498
- h = block(h)
1499
-
1500
- hashed = self.hash_routing
1501
- if self.v1_only:
1502
- h1, paths = self._walk_seq(
1503
- h, self.bank1, self.to_sphere1, record_path=record_path,
1504
- entry_ids=pezzi_ids if hashed else None,
1505
- )
1506
- h1 = self._descend(0, h1, pezzi_ids)
1507
- h = self.res_ln_h1(h1)
1508
- if len(self.attn_blocks) > 0:
1509
- h = self.attn_blocks[-1](h)
1510
- h = self.final_norm(h)
1511
- logits = self.lm_head(h)
1512
- logits = 30.0 * torch.tanh(logits / 30.0)
1513
- return logits, paths
1514
-
1515
- if parole_ids is None or unioni_ids is None:
1516
- raise ValueError("Cascade 3-stadi richiede parole_ids e unioni_ids")
1517
-
1518
- h1, _ = self._walk_seq(
1519
- h, self.bank1, self.to_sphere1, record_path=False, entry_ids=pezzi_ids if hashed else None
1520
- )
1521
- h1 = self._descend(0, h1, pezzi_ids)
1522
-
1523
- # Stage 2: fuse bilanciato (LN per-addendo) + emb parole (+ attn inter-stadio opzionale)
1524
- # v8.1 fuse_mask_pad: su pad/unk azzera l'addendo emb (mask-pad già saltava solo walk/piramide)
1525
- active2 = self._active_mask(1, parole_ids)
1526
- emb2 = self.fuse_ln_e2(self.emb_v2(parole_ids))
1527
- if self.fuse_mask_pad and active2 is not None:
1528
- emb2 = emb2 * active2.unsqueeze(-1).to(dtype=emb2.dtype)
1529
- h = self.fuse_norm2(self.fuse_ln_h2(self.proj_12(h1)) + emb2)
1530
- if self.attn_inter2 is not None:
1531
- h = self.attn_inter2(h)
1532
- h2, _ = self._walk_seq(
1533
- h,
1534
- self.bank2,
1535
- self.to_sphere2,
1536
- record_path=False,
1537
- entry_ids=parole_ids if hashed else None,
1538
- active_bt=active2,
1539
- )
1540
- h2 = self._descend(1, h2, parole_ids, active_bt=active2)
1541
-
1542
- # Stage 3: fuse bilanciato + emb unioni
1543
- active3 = self._active_mask(2, unioni_ids)
1544
- emb3 = self.fuse_ln_e3(self.emb_v3(unioni_ids))
1545
- if self.fuse_mask_pad and active3 is not None:
1546
- emb3 = emb3 * active3.unsqueeze(-1).to(dtype=emb3.dtype)
1547
- h = self.fuse_norm3(self.fuse_ln_h3(self.proj_23(h2)) + emb3)
1548
- h3, paths = self._walk_seq(
1549
- h,
1550
- self.bank3,
1551
- self.to_sphere3,
1552
- record_path=record_path,
1553
- entry_ids=unioni_ids if hashed else None,
1554
- active_bt=active3,
1555
- )
1556
- h3 = self._descend(2, h3, unioni_ids, active_bt=active3)
1557
-
1558
- # Residual bilanciato: norm per-stadio (con dropout 0.1 su res_ln_h1) + attn post-fuse
1559
- h = (
1560
- self.res_drop_h1(self.res_ln_h1(h1))
1561
- + self.res_ln_h2(h2)
1562
- + self.res_ln_h3(h3)
1563
- )
1564
- if len(self.attn_blocks) > 0:
1565
- h = self.attn_blocks[-1](h)
1566
- h = self.final_norm(h)
1567
- if hasattr(self, "watchman") and self.watchman is not None:
1568
- h_entangled = self.watchman.inject_geodesic_nonlinear_entanglement(h)
1569
- h = self.watchman(
1570
- h_entangled,
1571
- is_ethical=getattr(self, "_watchman_is_ethical", True),
1572
- license_text=getattr(self, "_watchman_license_text", ""),
1573
- )
1574
- logits = self.lm_head(h)
1575
- logits = 30.0 * torch.tanh(logits / 30.0)
1576
- return logits, paths
1577
-
1578
-
1579
- class V1OnlyMeshModel(CascadeMeshModel):
1580
- """Solo V1 (pezzi): emb_v1 (+pos) → CausalAttn_0 → bank1 (80 facce) → LN → CausalAttn_1 → final_norm → lm_head."""
1581
-
1582
- def __init__(
1583
- self,
1584
- vocab_pezzi: int,
1585
- d_model: int = 256,
1586
- frequency: int = 2,
1587
- walk: Optional[WalkConfig] = None,
1588
- d_hidden: Optional[int] = None,
1589
- n_attn_layers: int = 2,
1590
- n_heads: Optional[int] = None,
1591
- max_seq_len: int = 512,
1592
- attn_dropout: float = 0.0,
1593
- compass_spiral: bool = False,
1594
- fast_walk: bool = False,
1595
- ):
1596
- super().__init__(
1597
- vocab_pezzi=vocab_pezzi,
1598
- vocab_parole=0,
1599
- vocab_unioni=0,
1600
- d_model=d_model,
1601
- frequency=frequency,
1602
- walk=walk,
1603
- d_hidden=d_hidden,
1604
- n_attn_layers=n_attn_layers,
1605
- n_heads=n_heads,
1606
- max_seq_len=max_seq_len,
1607
- attn_dropout=attn_dropout,
1608
- res_h1_dropout=0.0,
1609
- v1_only=True,
1610
- compass_spiral=compass_spiral,
1611
- fast_walk=fast_walk,
1612
- )
1613
-
1614
-
1615
- def count_parameters(model: nn.Module) -> int:
1616
- return sum(p.numel() for p in model.parameters() if p.requires_grad)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
mesh_topology.py DELETED
@@ -1,352 +0,0 @@
1
- """
2
- Mesh geodetica sferica a tasselli triangolari.
3
-
4
- Parte da un icosaedro regolare e lo suddivide con frequenza `frequency`
5
- (ogni faccia → frequency² triangoli). Espone:
6
- - indici faccia → 3 vertici
7
- - tabella di adiacenza: 3 vicini per faccia (ordine lati 0,1,2)
8
- - packing a coppie di facce adiacenti per GEMM rettangolari
9
- """
10
-
11
- from __future__ import annotations
12
-
13
- from dataclasses import dataclass
14
- from typing import Dict, List, Optional, Tuple
15
-
16
- import numpy as np
17
-
18
- _PHI = (1.0 + np.sqrt(5.0)) / 2.0
19
-
20
-
21
- def _normalize(v: np.ndarray) -> np.ndarray:
22
- n = np.linalg.norm(v, axis=-1, keepdims=True)
23
- return v / np.maximum(n, 1e-12)
24
-
25
-
26
- def icosahedron_vertices() -> np.ndarray:
27
- """12 vertici icosaedro (ordine classico three.js / Wikipedia)."""
28
- verts = np.asarray(
29
- [
30
- [-1, _PHI, 0],
31
- [1, _PHI, 0],
32
- [-1, -_PHI, 0],
33
- [1, -_PHI, 0],
34
- [0, -1, _PHI],
35
- [0, 1, _PHI],
36
- [0, -1, -_PHI],
37
- [0, 1, -_PHI],
38
- [_PHI, 0, -1],
39
- [_PHI, 0, 1],
40
- [-_PHI, 0, -1],
41
- [-_PHI, 0, 1],
42
- ],
43
- dtype=np.float64,
44
- )
45
- return _normalize(verts)
46
-
47
-
48
- def icosahedron_faces() -> np.ndarray:
49
- """20 facce triangolari."""
50
- return np.asarray(
51
- [
52
- [0, 11, 5],
53
- [0, 5, 1],
54
- [0, 1, 7],
55
- [0, 7, 10],
56
- [0, 10, 11],
57
- [1, 5, 9],
58
- [5, 11, 4],
59
- [11, 10, 2],
60
- [10, 7, 6],
61
- [7, 1, 8],
62
- [3, 9, 4],
63
- [3, 4, 2],
64
- [3, 2, 6],
65
- [3, 6, 8],
66
- [3, 8, 9],
67
- [4, 9, 5],
68
- [2, 4, 11],
69
- [6, 2, 10],
70
- [8, 6, 7],
71
- [9, 8, 1],
72
- ],
73
- dtype=np.int64,
74
- )
75
-
76
-
77
- def _edge_key(a: int, b: int) -> Tuple[int, int]:
78
- return (a, b) if a < b else (b, a)
79
-
80
-
81
- @dataclass(frozen=True)
82
- class GeodesicMesh:
83
- frequency: int
84
- vertices: np.ndarray # (V, 3)
85
- faces: np.ndarray # (F, 3)
86
- adjacency: np.ndarray # (F, 3) vicino oltre lato i
87
- face_centroids: np.ndarray # (F, 3)
88
- pairs: np.ndarray # (P, 2) packing GEMM
89
- unpaired: np.ndarray # (U,)
90
- compass_spiral: bool = False
91
- spiral_order: Optional[np.ndarray] = None # (F,) face_id ordinati lungo la spirale polo→polo
92
- spiral_rank: Optional[np.ndarray] = None # (F,) rango 0..F-1 lungo la spirale per ogni face_id
93
- spiral_band: Optional[np.ndarray] = None # (F,) fascia latitudinale 0..B-1 per ogni face_id
94
-
95
- @property
96
- def num_faces(self) -> int:
97
- return int(self.faces.shape[0])
98
-
99
- @property
100
- def num_vertices(self) -> int:
101
- return int(self.vertices.shape[0])
102
-
103
-
104
- def build_geodesic_mesh(
105
- frequency: int = 2,
106
- *,
107
- compass_spiral: bool = False,
108
- ) -> GeodesicMesh:
109
- """
110
- Mesh geodetica a frequenza `frequency` (>= 1).
111
- Faces = 20 * frequency².
112
- Se `compass_spiral=True`, riordina i 3 vicini esistenti di ogni faccia
113
- lungo la spirale da polo a polo:
114
- porta 0 = avanti lungo la spirale,
115
- porta 1 = indietro lungo la spirale,
116
- porta 2 = cambio di fascia (nord/sud).
117
- """
118
- if frequency < 1:
119
- raise ValueError("frequency deve essere >= 1")
120
-
121
- base_v = icosahedron_vertices()
122
- base_f = icosahedron_faces()
123
-
124
- vert_list: List[np.ndarray] = [base_v[i].copy() for i in range(len(base_v))]
125
- # chiave: (lo, hi, steps_from_lo) → index, steps in 1..freq-1
126
- edge_cache: Dict[Tuple[int, int, int], int] = {}
127
-
128
- def point_on_edge(i: int, j: int, steps_from_i: int) -> int:
129
- """steps_from_i=0 → i, =frequency → j."""
130
- if steps_from_i == 0:
131
- return i
132
- if steps_from_i == frequency:
133
- return j
134
- lo, hi = _edge_key(i, j)
135
- steps_from_lo = steps_from_i if i == lo else frequency - steps_from_i
136
- key = (lo, hi, steps_from_lo)
137
- if key in edge_cache:
138
- return edge_cache[key]
139
- t = steps_from_lo / float(frequency)
140
- p = _normalize((1.0 - t) * base_v[lo] + t * base_v[hi])
141
- idx = len(vert_list)
142
- vert_list.append(p)
143
- edge_cache[key] = idx
144
- return idx
145
-
146
- faces: List[Tuple[int, int, int]] = []
147
-
148
- for v0, v1, v2 in base_f:
149
- v0, v1, v2 = int(v0), int(v1), int(v2)
150
- grid: Dict[Tuple[int, int], int] = {}
151
-
152
- for a in range(frequency + 1):
153
- for b in range(frequency + 1 - a):
154
- c = frequency - a - b # peso su v0
155
- # a verso v1, b verso v2
156
- if a == 0 and b == 0:
157
- grid[(a, b)] = v0
158
- elif b == 0:
159
- grid[(a, b)] = point_on_edge(v0, v1, a)
160
- elif a == 0:
161
- grid[(a, b)] = point_on_edge(v0, v2, b)
162
- elif c == 0:
163
- grid[(a, b)] = point_on_edge(v1, v2, b)
164
- else:
165
- p = _normalize(
166
- (c * base_v[v0] + a * base_v[v1] + b * base_v[v2]) / float(frequency)
167
- )
168
- idx = len(vert_list)
169
- vert_list.append(p)
170
- grid[(a, b)] = idx
171
-
172
- for a in range(frequency):
173
- for b in range(frequency - a):
174
- i00 = grid[(a, b)]
175
- i10 = grid[(a + 1, b)]
176
- i01 = grid[(a, b + 1)]
177
- faces.append((i00, i10, i01))
178
- if a + b + 1 < frequency:
179
- i11 = grid[(a + 1, b + 1)]
180
- faces.append((i10, i11, i01))
181
-
182
- vertices = _normalize(np.stack(vert_list, axis=0))
183
- faces_arr = np.asarray(faces, dtype=np.int64)
184
- expected = 20 * frequency * frequency
185
- if faces_arr.shape[0] != expected:
186
- raise RuntimeError(f"Attese {expected} facce, ottenute {faces_arr.shape[0]}")
187
-
188
- adjacency = build_adjacency(faces_arr)
189
- centroids = _normalize(vertices[faces_arr].mean(axis=1))
190
- spiral_adj, spiral_order, spiral_rank, spiral_band = reorder_adjacency_spiral_compass(
191
- adjacency, centroids
192
- )
193
- if compass_spiral:
194
- adjacency = spiral_adj
195
- pairs, unpaired = pack_face_pairs(adjacency)
196
-
197
- return GeodesicMesh(
198
- frequency=frequency,
199
- vertices=vertices,
200
- faces=faces_arr,
201
- adjacency=adjacency,
202
- face_centroids=centroids,
203
- pairs=pairs,
204
- unpaired=unpaired,
205
- compass_spiral=bool(compass_spiral),
206
- spiral_order=spiral_order,
207
- spiral_rank=spiral_rank,
208
- spiral_band=spiral_band,
209
- )
210
-
211
-
212
- def reorder_adjacency_spiral_compass(
213
- adjacency: np.ndarray,
214
- face_centroids: np.ndarray,
215
- ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
216
- """
217
- Ordina i centroidi lungo una spirale da polo a polo attorno all'asse polare
218
- icosaedrico (vertice 0 → vertice 3, simmetria 5-fold) e permuta i 3 vicini
219
- già presenti in `adjacency` per ogni faccia `f`:
220
- - porta 0: vicino «avanti» lungo la spirale (+Δlon sulla fascia),
221
- - porta 1: vicino «indietro» lungo la spirale (-Δlon sulla fascia),
222
- - porta 2: cambio di fascia (il terzo vicino, attraversamento nord/sud).
223
-
224
- Non aggiunge né rimuove archi o facce: per ogni f, set(new_adj[f]) == set(adjacency[f]).
225
- Restituisce (new_adj, spiral_order, spiral_rank, spiral_band).
226
- """
227
- n_faces = int(face_centroids.shape[0])
228
- if adjacency.shape != (n_faces, 3):
229
- raise ValueError(f"adjacency attesa ({n_faces}, 3), ricevuta {adjacency.shape}")
230
-
231
- # Asse polare 5-fold dell'icosaedro: polo nord = vertice 0, polo sud = vertice 3
232
- pole = icosahedron_vertices()[0]
233
- u3 = pole / np.linalg.norm(pole)
234
- ref = np.array([0.0, 0.0, 1.0], dtype=np.float64)
235
- u1 = ref - np.dot(ref, u3) * u3
236
- u1 /= np.linalg.norm(u1)
237
- u2 = np.cross(u3, u1)
238
-
239
- z = face_centroids @ u3 # latitudine coseno in [-1, +1] (polo nord = +1, polo sud = -1)
240
- lon = np.mod(np.arctan2(face_centroids @ u2, face_centroids @ u1), 2.0 * np.pi)
241
-
242
- new_adj = np.empty_like(adjacency)
243
- for f in range(n_faces):
244
- nbs = adjacency[f]
245
- # Differenza angolare ciclica in (-pi, +pi] rispetto alla faccia f
246
- dlons = [
247
- float(((lon[int(nb)] - lon[f] + np.pi) % (2.0 * np.pi)) - np.pi)
248
- for nb in nbs
249
- ]
250
- # Il vicino di cambio fascia (nord/sud) attraversa il lato orizzontale (|Δlon| minimo, |Δz| massimo);
251
- # gli altri due muovono lungo la fascia in senso +Δlon (avanti, porta 0) e -Δlon (indietro, porta 1).
252
- idx_band = int(np.argmin(np.abs(dlons)))
253
- rem = [i for i in range(3) if i != idx_band]
254
- if dlons[rem[0]] >= dlons[rem[1]]:
255
- idx_fwd, idx_bwd = rem[0], rem[1]
256
- else:
257
- idx_fwd, idx_bwd = rem[1], rem[0]
258
- new_adj[f, 0] = nbs[idx_fwd]
259
- new_adj[f, 1] = nbs[idx_bwd]
260
- new_adj[f, 2] = nbs[idx_band]
261
-
262
- # Costruisce l'ordine spirale da polo nord (+z) a polo sud (-z) seguendo i cicli di porta 0
263
- visited = np.zeros(n_faces, dtype=bool)
264
- bands: List[List[int]] = []
265
- for f in range(n_faces):
266
- if visited[f]:
267
- continue
268
- cyc: List[int] = []
269
- cur = f
270
- while not visited[cur]:
271
- visited[cur] = True
272
- cyc.append(cur)
273
- cur = int(new_adj[cur, 0])
274
- # Ruota il ciclo in modo che inizi dalla faccia con longitudine minima in [0, 2pi)
275
- start_idx = int(np.argmin([lon[idx] for idx in cyc]))
276
- cyc = cyc[start_idx:] + cyc[:start_idx]
277
- bands.append(cyc)
278
-
279
- # Ordina le fasce da polo nord (z massima) a polo sud (z minima)
280
- bands.sort(key=lambda cyc: -float(np.mean(z[cyc])))
281
-
282
- spiral_order_list: List[int] = []
283
- spiral_band = np.empty(n_faces, dtype=np.int64)
284
- for b_idx, cyc in enumerate(bands):
285
- for f_id in cyc:
286
- spiral_band[f_id] = b_idx
287
- spiral_order_list.append(f_id)
288
-
289
- spiral_order = np.asarray(spiral_order_list, dtype=np.int64)
290
- spiral_rank = np.empty(n_faces, dtype=np.int64)
291
- spiral_rank[spiral_order] = np.arange(n_faces, dtype=np.int64)
292
-
293
- return new_adj, spiral_order, spiral_rank, spiral_band
294
-
295
-
296
- def build_adjacency(faces: np.ndarray) -> np.ndarray:
297
- """adjacency[f, i] = faccia oltre lato i; lato0=(v0,v1), lato1=(v1,v2), lato2=(v2,v0)."""
298
- fcount = faces.shape[0]
299
- edge_to_face: Dict[Tuple[int, int], List[Tuple[int, int]]] = {}
300
- for fi, (a, b, c) in enumerate(faces):
301
- for li, (u, v) in enumerate(((int(a), int(b)), (int(b), int(c)), (int(c), int(a)))):
302
- edge_to_face.setdefault(_edge_key(u, v), []).append((fi, li))
303
-
304
- adj = np.full((fcount, 3), -1, dtype=np.int64)
305
- for occ in edge_to_face.values():
306
- if len(occ) != 2:
307
- continue
308
- (f0, l0), (f1, l1) = occ
309
- adj[f0, l0] = f1
310
- adj[f1, l1] = f0
311
-
312
- if np.any(adj < 0):
313
- bad = int(np.sum(adj < 0))
314
- raise RuntimeError(f"Adiacenza incompleta ({bad} lati aperti)")
315
- return adj
316
-
317
-
318
- def pack_face_pairs(adjacency: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
319
- """Matching greedy di facce adiacenti → matrici 2×D contigue."""
320
- fcount = adjacency.shape[0]
321
- used = np.zeros(fcount, dtype=bool)
322
- pairs: List[Tuple[int, int]] = []
323
- for f in range(fcount):
324
- if used[f]:
325
- continue
326
- partner = -1
327
- for n in adjacency[f]:
328
- ni = int(n)
329
- if not used[ni]:
330
- partner = ni
331
- break
332
- if partner < 0:
333
- continue
334
- used[f] = True
335
- used[partner] = True
336
- pairs.append((f, partner) if f < partner else (partner, f))
337
- unpaired = np.asarray([i for i in range(fcount) if not used[i]], dtype=np.int64)
338
- return np.asarray(pairs, dtype=np.int64), unpaired
339
-
340
-
341
- def nearest_face(mesh: GeodesicMesh, directions: np.ndarray) -> np.ndarray:
342
- """Argmax allineamento con centroidi faccia (prodotto scalare su sfera)."""
343
- d = _normalize(np.asarray(directions, dtype=np.float64))
344
- scores = d @ mesh.face_centroids.T
345
- return np.argmax(scores, axis=-1).astype(np.int64)
346
-
347
-
348
- def describe_mesh(mesh: GeodesicMesh) -> str:
349
- return (
350
- f"GeodesicMesh(freq={mesh.frequency}, V={mesh.num_vertices}, "
351
- f"F={mesh.num_faces}, pairs={len(mesh.pairs)}, unpaired={len(mesh.unpaired)})"
352
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
model.safetensors DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:d3b4199512b96b379243eb5add268792129830bb2bc8700bdbf0acd3fccd4c12
3
- size 180368492
 
 
 
 
modeling_geodesic.py DELETED
@@ -1,228 +0,0 @@
1
- # ============================================================================
2
- # LEGAL NOTICE & WATERMARK: R08 NON-COMMERCIAL RESEARCH & ETHICAL LICENSE v2.0
3
- # Copyright (c) 2026 Thomas Seri (Sentinel). All Rights Reserved.
4
- # Exclusive Jurisdiction: Court of Milan (Sezione Specializzata Impresa), Italy.
5
- # Clean-room reverse engineering, distillation, commercial or military use
6
- # is strictly prohibited and constitutes a breach of contract.
7
- # ============================================================================
8
- __author__ = "Thomas Seri (Sentinel)"
9
- __license__ = "R08-Non-Commercial-Ethical-License-v2.0"
10
- __jurisdiction__ = "Court of Milan, Italy"
11
-
12
- #!/usr/bin/env python3
13
- """R08 (Rob) — Sentinel LifeOS Geodesic Mesh Neural Network (Pyramidal Sphere 50M)
14
- Official Hugging Face / PyTorch implementation.
15
- Author: Thomas Seri (https://www.sentinelhub.eu)
16
- """
17
- from __future__ import annotations
18
-
19
- import json
20
- from pathlib import Path
21
- from typing import List, Optional, Tuple, Union
22
-
23
- import torch
24
- import torch.nn as nn
25
- from safetensors.torch import load_file
26
-
27
- from geodesic_network import WalkConfig
28
- from geodesic_stages_model import CascadeMeshModel
29
- import pezzi_tokenizer as pt
30
- from vocab_stages import (
31
- StageBundle,
32
- StageEncoded,
33
- align_stages_to_pezzi,
34
- load_bundle,
35
- pass_to_v2,
36
- pass_to_v3,
37
- )
38
-
39
-
40
- def realign_from_pezzi(
41
- pezzi_ids: list[int], bundle: StageBundle, causal_align: bool = True
42
- ) -> Tuple[list[int], list[int], list[int]]:
43
- """Ricostruisce lo stato multi-stadio (pezzi, parole, unioni) in modo causale."""
44
- v2 = pass_to_v2(pezzi_ids, bundle)
45
- v3 = pass_to_v3(v2, bundle)
46
- enc = StageEncoded(
47
- pezzi_ids=list(pezzi_ids),
48
- parole_ids=list(v2.parole_ids),
49
- unioni_ids=list(v3.unioni_ids),
50
- spans=[],
51
- v2=v2,
52
- v3=v3,
53
- )
54
- p, w, u = align_stages_to_pezzi(enc, bundle, causal=causal_align)
55
- return list(p), list(w), list(u)
56
-
57
-
58
- class R08Tokenizer:
59
- """Tokenizer a cascata a 3 stadi per R08 (Rob)."""
60
-
61
- def __init__(self, bundle: StageBundle):
62
- self.bundle = bundle
63
- self.pezzi = bundle.pezzi
64
- self.eos_id = int(getattr(bundle.pezzi, "eos_id", 2))
65
- self.pad_id = int(getattr(bundle.pezzi, "pad_id", 3))
66
-
67
- @classmethod
68
- def from_pretrained(cls, pretrained_path: Union[str, Path]) -> "R08Tokenizer":
69
- root = Path(pretrained_path)
70
- vocab_dir = root / "vocab_stages" if (root / "vocab_stages").exists() else root
71
- bundle = load_bundle(vocab_dir)
72
- return cls(bundle)
73
-
74
- def encode(self, text: str) -> dict:
75
- """Codifica il testo nei 3 stadi sincronizzati (pezzi, parole, unioni)."""
76
- enc = pt.encode_cascade(text, self.bundle)
77
- p, w, u = align_stages_to_pezzi(enc, self.bundle, causal=True)
78
- return {
79
- "pezzi": list(p),
80
- "parole": list(w),
81
- "unioni": list(u),
82
- }
83
-
84
- def decode(self, token_ids: List[int]) -> str:
85
- """Decodifica gli ID token dei pezzi in testo UTF-8."""
86
- return pt.decode(token_ids, self.pezzi)
87
-
88
-
89
- class R08Model(nn.Module):
90
- """Modello neurale geodetico-piramidale R08 (Rob) da 45M parametri."""
91
-
92
- def __init__(self, config: dict, mesh_model: CascadeMeshModel):
93
- super().__init__()
94
- self.config = config
95
- self.mesh_model = mesh_model
96
-
97
- @classmethod
98
- def from_pretrained(
99
- cls,
100
- pretrained_path: Union[str, Path],
101
- device: Optional[str] = None,
102
- torch_dtype: Optional[torch.dtype] = None,
103
- ) -> "R08Model":
104
- root = Path(pretrained_path)
105
- cfg_file = root / "config.json"
106
- if not cfg_file.exists():
107
- raise FileNotFoundError(f"config.json non trovato in {root}")
108
-
109
- with open(cfg_file, "r", encoding="utf-8") as f:
110
- config = json.load(f)
111
-
112
- walk = WalkConfig(
113
- max_hops=1,
114
- exit_threshold=0.85,
115
- hard_routing=False,
116
- router_temperature=1.0,
117
- )
118
-
119
- mesh_model = CascadeMeshModel(
120
- vocab_pezzi=config.get("vocab_pezzi", 1251),
121
- vocab_parole=config.get("vocab_parole", 17908),
122
- vocab_unioni=config.get("vocab_unioni", 20006),
123
- d_model=config.get("d_model", 256),
124
- frequency=config.get("mesh_topology", {}).get("frequency", 2),
125
- walk=walk,
126
- n_attn_layers=config.get("n_attn_layers", 2),
127
- n_heads=config.get("n_heads", 8),
128
- max_seq_len=config.get("max_seq_len", 512),
129
- compass_spiral=config.get("routing", {}).get("compass_spiral", True),
130
- fast_walk=True,
131
- router_v6=config.get("routing", {}).get("router_v6", True),
132
- hash_routing=config.get("routing", {}).get("hash_routing", True),
133
- no_walk=config.get("routing", {}).get("no_walk", True),
134
- pyramid_center=config.get("pyramid_center", True),
135
- )
136
-
137
- weights_file = root / "model.safetensors"
138
- if not weights_file.exists():
139
- raise FileNotFoundError(f"model.safetensors non trovato in {root}")
140
-
141
- state_dict = load_file(str(weights_file))
142
- mesh_model.load_state_dict(state_dict, strict=False)
143
- if hasattr(mesh_model, "watchman") and mesh_model.watchman is not None:
144
- mesh_model._watchman_license_text = mesh_model.watchman.verify_disk_license(root)
145
- mesh_model._watchman_is_ethical = True
146
-
147
- if device is None:
148
- if torch.backends.mps.is_available():
149
- device = "mps"
150
- elif torch.cuda.is_available():
151
- device = "cuda"
152
- else:
153
- device = "cpu"
154
-
155
- mesh_model.to(torch.device(device))
156
- if torch_dtype is not None:
157
- mesh_model.to(dtype=torch_dtype)
158
-
159
- mesh_model.eval()
160
- return cls(config, mesh_model)
161
-
162
- def forward(self, pezzi, parole, unioni, record_path: bool = False):
163
- return self.mesh_model(pezzi, parole, unioni, record_path=record_path)
164
-
165
- @torch.inference_mode()
166
- def generate(
167
- self,
168
- prompt: str,
169
- tokenizer: R08Tokenizer,
170
- max_new_tokens: int = 48,
171
- temperature: float = 0.7,
172
- top_p: float = 0.9,
173
- repetition_penalty: float = 1.15,
174
- ) -> str:
175
- """Generazione di testo autoregressiva con sampling controllato e penalità di ripetizione."""
176
- if hasattr(self.mesh_model, "watchman") and self.mesh_model.watchman is not None:
177
- is_ok, reason = self.mesh_model.watchman.scan_security_and_ethics(prompt)
178
- if not is_ok:
179
- self.mesh_model._watchman_is_ethical = False
180
- return f"[WATCHMAN KEYSTONE — ACCESSO NEGATO] Richiesta non consentita ({reason})."
181
- self.mesh_model._watchman_is_ethical = True
182
- dev = next(self.mesh_model.parameters()).device
183
- enc = tokenizer.encode(prompt)
184
- p, w, u = enc["pezzi"], enc["parole"], enc["unioni"]
185
- if not p:
186
- p, w, u = [tokenizer.eos_id], [tokenizer.bundle.parole.pad_id], [tokenizer.bundle.unioni.pad_id]
187
-
188
- new_tokens: list[int] = []
189
- for _ in range(max_new_tokens):
190
- seq_p = torch.tensor([p[-256:]], dtype=torch.long, device=dev)
191
- seq_w = torch.tensor([w[-256:]], dtype=torch.long, device=dev)
192
- seq_u = torch.tensor([u[-256:]], dtype=torch.long, device=dev)
193
-
194
- logits, _ = self.mesh_model(seq_p, seq_w, seq_u, record_path=False)
195
- next_logits = logits[0, -1]
196
-
197
- # Repetition penalty
198
- if repetition_penalty != 1.0 and new_tokens:
199
- for token_id in set(new_tokens):
200
- if next_logits[token_id] > 0:
201
- next_logits[token_id] /= repetition_penalty
202
- else:
203
- next_logits[token_id] *= repetition_penalty
204
-
205
- # Temperature & Top-p Sampling
206
- if temperature > 0:
207
- probs = torch.softmax(next_logits / temperature, dim=-1)
208
- if top_p < 1.0:
209
- sorted_probs, sorted_indices = torch.sort(probs, descending=True)
210
- cumulative_probs = torch.cumsum(sorted_probs, dim=-1)
211
- sorted_indices_to_remove = cumulative_probs > top_p
212
- sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
213
- sorted_indices_to_remove[..., 0] = 0
214
- indices_to_remove = sorted_indices[sorted_indices_to_remove]
215
- probs[indices_to_remove] = 0.0
216
- probs = probs / probs.sum()
217
- nxt = int(torch.multinomial(probs, num_samples=1).item())
218
- else:
219
- nxt = int(next_logits.argmax().item())
220
-
221
- if nxt in (tokenizer.eos_id, tokenizer.pad_id):
222
- break
223
-
224
- new_tokens.append(nxt)
225
- p.append(nxt)
226
- p, w, u = realign_from_pezzi(p, tokenizer.bundle, causal_align=True)
227
-
228
- return tokenizer.decode(new_tokens)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pezzi_tokenizer.py DELETED
@@ -1,228 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Tokenizer pezzi goTommy2 locale: sostituisce ``gptommy.encode`` senza bytecode in /tmp.
3
-
4
- ``r08/gptommy/encode.py`` è un wrapper che carica il bytecode originale da
5
- ``/tmp/encode_ORIGINAL.cpython-314.pyc`` (cancellato dalla pulizia di macOS) o da
6
- ``__pycache__`` solo se >1000 byte; lì c'è ormai il wrapper stesso ricompilato
7
- (722 byte), quindi l'import fallisce. Il sorgente originale non esiste più.
8
-
9
- Il contratto dell'encoder sopravvive nel docstring del bytecode originale di
10
- ``pipeline.cpython-314.pyc`` (20 set): copia in
11
- ``vendor/gptommy_pycache_backup_20261019/``, da leggere con marshal senza
12
- importare ``gptommy.pipeline`` (l'import sovrascriverebbe l'originale in r08):
13
-
14
- 1. NFC 2. speciali letterali ``<|...|>`` 3. whitespace (spazio→<spazio>,
15
- \\n→<riga>, \\t→<tab>, \\r→<cr>, NBSP→<nbsp>; pezzi indent se esistono)
16
- 4. cifre: coppie 00–99 se in vocab, altrimenti cifra singola
17
- 5. longest-match greedy intra-parola, fallback 1 carattere, poi <|unk|>
18
- 6. case: match sul testo così com'è, poi ``lower()`` se non c'è pezzo.
19
-
20
- Col vocab pezzi 1251 (nessuna coppia di cifre, nessun pezzo con spazi o indent,
21
- nessun pezzo multi-carattere con maiuscole) il contratto coincide con
22
- ``vocab_stages.encode_pezzi_fallback``. ``encode`` replica esattamente quella
23
- lettura (``TRAINING_VARIANT``); le altre letture ammesse dal testo del contratto
24
- restano disponibili via ``encode_variant`` solo per la verifica
25
- (``verify_tokenizer_equivalence.py``).
26
- """
27
- from __future__ import annotations
28
-
29
- import unicodedata
30
- from dataclasses import dataclass
31
- from pathlib import Path
32
- from typing import Dict, List, Optional, Sequence, Tuple
33
-
34
- ROOT = Path(__file__).resolve().parent
35
- GOTTOMMY2_VOCAB = Path("/Users/thomasseri/Desktop/columns/logs/goTommy2_vocab_by_id.txt")
36
- LOCAL_PEZZI_VOCAB = ROOT / "vocab_stages" / "00_pezzi_by_id.txt"
37
-
38
- WS_PIECES = {"\n": "<riga>", "\t": "<tab>", "\r": "<cr>", "\xa0": "<nbsp>", " ": "<spazio>"}
39
- DECODE_WS = {"<spazio>": " ", "<riga>": "\n", "<tab>": "\t"}
40
-
41
- TRAINING_VARIANT = {
42
- "nfc": False,
43
- "literal_specials": False,
44
- "case_rule": "per_length",
45
- "unicode_ws": "isspace",
46
- }
47
-
48
-
49
- @dataclass
50
- class Vocab:
51
- id_to_token: List[str]
52
- token_to_id: Dict[str, int]
53
- path: Optional[Path] = None
54
-
55
-
56
- def load_vocab(path: Path | str | None = None) -> Vocab:
57
- """Stesso formato TSV di ``gptommy.vocab.load_vocab`` (codice, scheda, riga, id, pezzo)."""
58
- p = Path(path) if path is not None else (
59
- LOCAL_PEZZI_VOCAB if LOCAL_PEZZI_VOCAB.is_file() else GOTTOMMY2_VOCAB
60
- )
61
- id_to_token: Dict[int, str] = {}
62
- with p.open("r", encoding="utf-8") as fh:
63
- for line in fh:
64
- line = line.strip()
65
- if not line or line.startswith("#"):
66
- continue
67
- parts = line.split("\t")
68
- if len(parts) < 5:
69
- continue
70
- id_to_token[int(parts[3])] = parts[4]
71
- if not id_to_token:
72
- raise RuntimeError(f"vocab vuoto: {p}")
73
- n = max(id_to_token) + 1
74
- tokens = [id_to_token.get(i, f"<unk_{i}>") for i in range(n)]
75
- return Vocab(id_to_token=tokens, token_to_id={t: i for i, t in enumerate(tokens)}, path=p)
76
-
77
-
78
- class _Tables:
79
- __slots__ = ("max_len", "specials", "unk_id")
80
-
81
- def __init__(self, vocab) -> None:
82
- toks = vocab.id_to_token
83
- self.max_len = max((len(t) for t in toks if not t.startswith("<")), default=1)
84
- self.specials = sorted(
85
- (t for t in toks if t.startswith("<") and len(t) > 1 and t.endswith(">")),
86
- key=len,
87
- reverse=True,
88
- )
89
- self.unk_id = int(vocab.token_to_id.get("<|unk|>", 4))
90
-
91
-
92
- _TABLE_CACHE: Dict[int, tuple] = {}
93
-
94
-
95
- def _tables(vocab) -> _Tables:
96
- key = id(vocab.token_to_id)
97
- hit = _TABLE_CACHE.get(key)
98
- if hit is not None and hit[0] is vocab.token_to_id:
99
- return hit[1]
100
- tab = _Tables(vocab)
101
- _TABLE_CACHE[key] = (vocab.token_to_id, tab)
102
- return tab
103
-
104
-
105
- def _is_ws(ch: str, unicode_ws: str) -> bool:
106
- if unicode_ws == "isspace":
107
- return ch.isspace()
108
- return ch in WS_PIECES
109
-
110
-
111
- def encode_variant(
112
- text: str,
113
- vocab,
114
- *,
115
- nfc: bool = False,
116
- literal_specials: bool = False,
117
- case_rule: str = "per_length",
118
- unicode_ws: str = "isspace",
119
- ) -> List[int]:
120
- """Encoder del contratto gpTommy con le letture alternative esplicite.
121
-
122
- case_rule:
123
- ``per_length`` a ogni lunghezza prova il frammento così com'è, poi lower()
124
- se alfabetico (= encode_pezzi_fallback; "Questo" → questo)
125
- ``per_position`` lower() solo se nessun pezzo matcha così com'è a quella
126
- posizione ("Questo" → Q u e s t o, visto che A–Z esistono)
127
- unicode_ws:
128
- ``isspace`` ogni carattere str.isspace() è whitespace (default <spazio>)
129
- ``listed`` solo spazio, \\n, \\t, \\r, NBSP; gli altri vanno al longest-match
130
- literal_specials: speciali ``<...>`` di qualunque lunghezza consumati per primi
131
- (il longest-match generico arriva solo a max_len=8 caratteri).
132
- """
133
- if case_rule not in ("per_length", "per_position"):
134
- raise ValueError(f"case_rule sconosciuta: {case_rule}")
135
- s = unicodedata.normalize("NFC", text) if nfc else text
136
- t2i = vocab.token_to_id
137
- tab = _tables(vocab)
138
- ids: List[int] = []
139
- n = len(s)
140
- i = 0
141
- while i < n:
142
- ch = s[i]
143
- if _is_ws(ch, unicode_ws):
144
- tid = t2i.get(WS_PIECES.get(ch, "<spazio>"))
145
- if tid is not None:
146
- ids.append(tid)
147
- i += 1
148
- continue
149
- if literal_specials and ch == "<":
150
- hit = next((sp for sp in tab.specials if s.startswith(sp, i)), None)
151
- if hit is not None:
152
- ids.append(t2i[hit])
153
- i += len(hit)
154
- continue
155
- upper = min(n, i + tab.max_len)
156
- tid = None
157
- j = upper
158
- while j > i:
159
- frag = s[i:j]
160
- tid = t2i.get(frag)
161
- if tid is None and case_rule == "per_length" and frag.isalpha():
162
- tid = t2i.get(frag.lower())
163
- if tid is not None:
164
- break
165
- j -= 1
166
- if tid is None and case_rule == "per_position":
167
- j = upper
168
- while j > i:
169
- frag = s[i:j]
170
- if frag.isalpha():
171
- tid = t2i.get(frag.lower())
172
- if tid is not None:
173
- break
174
- j -= 1
175
- if tid is not None:
176
- ids.append(tid)
177
- i = j
178
- continue
179
- tid = t2i.get(ch)
180
- if tid is None and ch.isalpha():
181
- tid = t2i.get(ch.lower())
182
- ids.append(tab.unk_id if tid is None else tid)
183
- i += 1
184
- return ids
185
-
186
-
187
- def encode(text: str, vocab) -> List[int]:
188
- """Encode pezzi usato dal training (stessa lettura di encode_pezzi_fallback)."""
189
- return encode_variant(text, vocab, **TRAINING_VARIANT)
190
-
191
-
192
- def decode(ids: Sequence[int], vocab) -> str:
193
- """Concatena i pezzi; sostituisce solo <spazio>/<riga>/<tab> (contratto gpTommy)."""
194
- toks = vocab.id_to_token
195
- out: List[str] = []
196
- for tid in ids:
197
- tok = toks[tid] if 0 <= int(tid) < len(toks) else "<|unk|>"
198
- out.append(DECODE_WS.get(tok, tok))
199
- return "".join(out)
200
-
201
-
202
- def cascade_from_pezzi(pezzi_ids: Sequence[int], bundle):
203
- """StageEncoded da pezzi già codificati: pass_to_v2 → pass_to_v3 (come encode_cascade)."""
204
- from vocab_stages import StageEncoded, pass_to_v2, pass_to_v3
205
-
206
- ids = [int(x) for x in pezzi_ids]
207
- v2 = pass_to_v2(ids, bundle)
208
- v3 = pass_to_v3(v2, bundle)
209
- return StageEncoded(
210
- pezzi_ids=ids,
211
- parole_ids=list(v2.parole_ids),
212
- unioni_ids=list(v3.unioni_ids),
213
- spans=[],
214
- v2=v2,
215
- v3=v3,
216
- )
217
-
218
-
219
- def encode_cascade(text: str, bundle):
220
- """vocab_stages.encode_cascade con V1 = ``encode`` locale (mai gptommy)."""
221
- return cascade_from_pezzi(encode(text, bundle.pezzi), bundle)
222
-
223
-
224
- def encode_stages_aligned(text: str, bundle) -> Tuple[List[int], List[int], List[int]]:
225
- """(pezzi, parole, unioni) allineati sulla timeline pezzi, come nel training."""
226
- from vocab_stages import align_stages_to_pezzi
227
-
228
- return align_stages_to_pezzi(encode_cascade(text, bundle), bundle)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
test_inference_release.py DELETED
@@ -1,31 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Test di inferenza ufficiale per il pacchetto standalone R08 (Rob) su Hugging Face / GitHub."""
3
- from pathlib import Path
4
- from modeling_geodesic import R08Model, R08Tokenizer
5
-
6
- ROOT = Path(__file__).resolve().parent
7
-
8
- def main():
9
- print("1. Caricamento tokenizer da vocab_stages...")
10
- tokenizer = R08Tokenizer.from_pretrained(ROOT)
11
-
12
- print("2. Caricamento modello R08 da model.safetensors...")
13
- model = R08Model.from_pretrained(ROOT, device="cpu")
14
- total_params = sum(p.numel() for p in model.parameters())
15
- print(f"Modello caricato con successo! Parametri: {total_params:,}")
16
-
17
- prompts = [
18
- "Utente: Ciao, come stai?\nAssistente:",
19
- "La capitale d'Italia è",
20
- "Per riparare un cilindro pneumatico Festo occorre",
21
- ]
22
-
23
- for p in prompts:
24
- print(f"\n--- PROMPT: {p} ---")
25
- out = model.generate(p, tokenizer, max_new_tokens=40, temperature=0.7, top_p=0.9, repetition_penalty=1.15)
26
- print(f"RISPOSTA:\n{out.strip()}")
27
-
28
- print("\n✅ Verifica completata con successo!")
29
-
30
- if __name__ == "__main__":
31
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vocab_stages.py DELETED
@@ -1,971 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Vocab a stadi — pipeline SEQUENZIALE a cascata (NON encode greedy parallelo).
3
-
4
- Flusso utente (testo → V1 → V2 → V3):
5
-
6
- 1. Vocab 1 (pezzi ~1200, goTommy2): spezzetta TUTTO il testo in token base.
7
- 2. Quei token passano a Vocab 2 (parole): crea/modifica/aggrega pezzi → parole
8
- (stesso tipo di passaggio: sequenza in → sequenza aggregata out).
9
- 3. L'uscita di V2 passa a Vocab 3 (unioni): unisce 2/3 parole se frequenti.
10
-
11
- API encode:
12
- encode_v1(text, pezzi) → pezzi_ids
13
- pass_to_v2(pezzi_ids, bundle) → parole_ids (+ surfaces)
14
- pass_to_v3(parole_ids|surfaces, bundle) → unioni stream
15
- encode_cascade(text, bundle) = V1 → V2 → V3
16
- align_stages_to_pezzi: ancora causale + carry-forward post-ancora
17
- (pad solo pre-prima-ancora; dopo, ultima parola/unione resta viva su spazi/pezzi)
18
-
19
- Build:
20
- vocab2 imparato da parole ricostruite sulle sequenze di pezzi (V1);
21
- vocab3 da unioni frequenti sull'uscita di V2.
22
-
23
- Wiring train (proposta, NON avviato): 3 stadi in SERIE
24
- emb_v1(pezzi) → mesh1 → transform → emb_v2(parole) → mesh2 → … → emb_v3(unioni).
25
- Non 3 stream paralleli “prova unioni|parole|pezzi”. Serve --confirm-stages-train.
26
-
27
- Deprecato: encode_stages greedy “unioni poi parole poi pezzi” (ora alias di cascade).
28
- """
29
- from __future__ import annotations
30
-
31
- import json
32
- import warnings
33
- from collections import Counter
34
- from dataclasses import dataclass, field
35
- from pathlib import Path
36
- from typing import Iterable, Iterator, Optional, Sequence, Union
37
-
38
- ROOT = Path(__file__).resolve().parent
39
- DEFAULT_OUT = ROOT / "vocab_stages"
40
- DEFAULT_PEZZI = Path("/Users/thomasseri/Desktop/columns/logs/goTommy2_vocab_by_id.txt")
41
-
42
- SPECIALS = [
43
- "<|end|>",
44
- "<|bos|>",
45
- "<|eos|>",
46
- "<|pad|>",
47
- "<|unk|>",
48
- "<|sep|>",
49
- ]
50
-
51
- # pezzi che rompono la parola in V2 (aggregazione pezzi → parola)
52
- _WORD_BREAK_PEZZI = frozenset(
53
- {
54
- "<spazio>",
55
- " ",
56
- "\t",
57
- "\n",
58
- ".",
59
- ",",
60
- ";",
61
- ":",
62
- "!",
63
- "?",
64
- "(",
65
- ")",
66
- "[",
67
- "]",
68
- "{",
69
- "}",
70
- '"',
71
- "'",
72
- "«",
73
- "»",
74
- "-",
75
- "—",
76
- "/",
77
- "\\",
78
- }
79
- )
80
-
81
-
82
- @dataclass
83
- class StageVocab:
84
- name: str
85
- id_to_token: list[str]
86
- token_to_id: dict[str, int]
87
- path: Optional[Path] = None
88
- meta: dict = field(default_factory=dict)
89
-
90
- def __len__(self) -> int:
91
- return len(self.id_to_token)
92
-
93
- @property
94
- def unk_id(self) -> int:
95
- return int(self.token_to_id.get("<|unk|>", 4))
96
-
97
- @property
98
- def eos_id(self) -> int:
99
- return int(self.token_to_id.get("<|eos|>", 2))
100
-
101
- @property
102
- def pad_id(self) -> int:
103
- return int(self.token_to_id.get("<|pad|>", 3))
104
-
105
-
106
- @dataclass
107
- class StageBundle:
108
- """Tre vocab separati + percorsi (id-space distinti)."""
109
-
110
- pezzi: StageVocab
111
- parole: StageVocab
112
- unioni: StageVocab
113
- root: Path
114
-
115
- def sizes(self) -> dict[str, int]:
116
- return {
117
- "pezzi": len(self.pezzi),
118
- "parole": len(self.parole),
119
- "unioni": len(self.unioni),
120
- }
121
-
122
-
123
- @dataclass
124
- class V1Out:
125
- """Uscita Vocab 1: tutto il testo spezzettato in pezzi."""
126
-
127
- pezzi_ids: list[int]
128
- surfaces: list[str]
129
-
130
-
131
- @dataclass
132
- class V2Out:
133
- """Uscita Vocab 2: pezzi aggregati in parole (passaggio da V1)."""
134
-
135
- parole_ids: list[int]
136
- surfaces: list[str]
137
- # gruppi di pezzi_ids che hanno formato ciascuna parola
138
- pezzi_groups: list[list[int]] = field(default_factory=list)
139
-
140
-
141
- @dataclass
142
- class V3Out:
143
- """Uscita Vocab 3: unioni 2/3 parole sulle superfici di V2.
144
-
145
- `stream` è la sequenza a cascata: ogni item è
146
- ("unione", surface, uid) oppure ("parola", surface, wid)
147
- dopo merge greedy trigram→bigram. `unioni_ids` allinea a parole V2
148
- (uid se la parola apre un'unione, altrimenti -1).
149
- """
150
-
151
- unioni_ids: list[int]
152
- stream: list[tuple[str, str, int]]
153
- surfaces: list[str] = field(default_factory=list)
154
-
155
-
156
- @dataclass
157
- class StageEncoded:
158
- """Risultato cascade V1→V2→V3 (id space distinti)."""
159
-
160
- pezzi_ids: list[int]
161
- parole_ids: list[int]
162
- unioni_ids: list[int]
163
- # traccia: ("v1"|"v2"|"v3", surface, id_or_ids)
164
- spans: list[tuple[str, str, object]]
165
- v1: Optional[V1Out] = None
166
- v2: Optional[V2Out] = None
167
- v3: Optional[V3Out] = None
168
-
169
-
170
- # ---------------------------------------------------------------------------
171
- # Vocab I/O
172
- # ---------------------------------------------------------------------------
173
-
174
-
175
- def load_pezzi_vocab(path: Path | str | None = None) -> StageVocab:
176
- """Carica goTommy2 (~1251) come stadio pezzi (V1)."""
177
- p = Path(path) if path is not None else DEFAULT_PEZZI
178
- id_to_token: dict[int, str] = {}
179
- with p.open("r", encoding="utf-8") as fh:
180
- for line in fh:
181
- line = line.strip()
182
- if not line or line.startswith("#"):
183
- continue
184
- parts = line.split("\t")
185
- if len(parts) < 5:
186
- continue
187
- gid = int(parts[3])
188
- id_to_token[gid] = parts[4]
189
- if not id_to_token:
190
- raise RuntimeError(f"pezzi vocab vuoto: {p}")
191
- n = max(id_to_token) + 1
192
- tokens = [id_to_token.get(i, f"<unk_{i}>") for i in range(n)]
193
- return StageVocab(
194
- name="pezzi",
195
- id_to_token=tokens,
196
- token_to_id={t: i for i, t in enumerate(tokens)},
197
- path=p,
198
- meta={"source": str(p), "note": "goTommy2-scale ~1200 (1251)"},
199
- )
200
-
201
-
202
- def _with_specials(tokens: Iterable[str]) -> list[str]:
203
- seen = set(SPECIALS)
204
- out = list(SPECIALS)
205
- for t in tokens:
206
- if t in seen or not t:
207
- continue
208
- seen.add(t)
209
- out.append(t)
210
- return out
211
-
212
-
213
- def make_stage_vocab(name: str, tokens: Iterable[str], *, meta: Optional[dict] = None) -> StageVocab:
214
- id_to_token = _with_specials(tokens)
215
- return StageVocab(
216
- name=name,
217
- id_to_token=id_to_token,
218
- token_to_id={t: i for i, t in enumerate(id_to_token)},
219
- meta=dict(meta or {}),
220
- )
221
-
222
-
223
- def save_stage_vocab(vocab: StageVocab, path: Path) -> None:
224
- path.parent.mkdir(parents=True, exist_ok=True)
225
- with path.open("w", encoding="utf-8") as fh:
226
- fh.write(f"# stage={vocab.name} size={len(vocab)}\n")
227
- fh.write("# colonne: codice\\tscheda\\triga\\tid\\tpezzo\n")
228
- for i, tok in enumerate(vocab.id_to_token):
229
- scheda = 0 if i < 1000 else 1
230
- riga = i if i < 1000 else (i - 1000)
231
- code = f"{scheda:02d}{riga:03d}"
232
- fh.write(f"{code}\t{scheda:02d}\t{riga:03d}\t{i}\t{tok}\n")
233
- meta_path = path.with_suffix(".meta.json")
234
- blob = {
235
- "name": vocab.name,
236
- "size": len(vocab),
237
- "path": str(path),
238
- "meta": vocab.meta,
239
- "specials": SPECIALS,
240
- }
241
- meta_path.write_text(json.dumps(blob, ensure_ascii=False, indent=2), encoding="utf-8")
242
- vocab.path = path
243
-
244
-
245
- def load_stage_vocab(path: Path, name: Optional[str] = None) -> StageVocab:
246
- id_to_token: dict[int, str] = {}
247
- with path.open("r", encoding="utf-8") as fh:
248
- for line in fh:
249
- line = line.strip()
250
- if not line or line.startswith("#"):
251
- continue
252
- parts = line.split("\t")
253
- if len(parts) < 5:
254
- continue
255
- id_to_token[int(parts[3])] = parts[4]
256
- n = max(id_to_token) + 1 if id_to_token else 0
257
- tokens = [id_to_token.get(i, f"<unk_{i}>") for i in range(n)]
258
- meta: dict = {}
259
- meta_path = path.with_suffix(".meta.json")
260
- if meta_path.is_file():
261
- try:
262
- meta = json.loads(meta_path.read_text(encoding="utf-8")).get("meta", {})
263
- except json.JSONDecodeError:
264
- meta = {}
265
- return StageVocab(
266
- name=name or path.stem,
267
- id_to_token=tokens,
268
- token_to_id={t: i for i, t in enumerate(tokens)},
269
- path=path,
270
- meta=meta,
271
- )
272
-
273
-
274
- def save_bundle(bundle: StageBundle, root: Optional[Path] = None) -> Path:
275
- out = Path(root) if root is not None else bundle.root
276
- out.mkdir(parents=True, exist_ok=True)
277
- pezzi_dst = out / "00_pezzi_by_id.txt"
278
- parole_path = out / "01_parole_by_id.txt"
279
- unioni_path = out / "02_unioni_by_id.txt"
280
- save_stage_vocab(bundle.pezzi, pezzi_dst)
281
- save_stage_vocab(bundle.parole, parole_path)
282
- save_stage_vocab(bundle.unioni, unioni_path)
283
- src = bundle.pezzi.meta.get("source") or str(DEFAULT_PEZZI)
284
- (out / "00_pezzi_SOURCE.txt").write_text(
285
- f"source={src}\nsize={len(bundle.pezzi)}\n", encoding="utf-8"
286
- )
287
-
288
- manifest = {
289
- "design": "multi_vocab_stages_cascade",
290
- "note": (
291
- "Tre id-space separati in SERIE: testo→V1 pezzi (~1200)→V2 parole→V3 unioni. "
292
- "NON encode greedy parallelo. ~1200 = scala goTommy2 (1251), non 1.2M."
293
- ),
294
- "stages": {
295
- "0_pezzi": {"path": str(pezzi_dst), "size": len(bundle.pezzi)},
296
- "1_parole": {"path": str(parole_path), "size": len(bundle.parole)},
297
- "2_unioni": {"path": str(unioni_path), "size": len(bundle.unioni)},
298
- },
299
- "encode": (
300
- "encode_cascade = encode_v1 → pass_to_v2 → pass_to_v3. "
301
- "Wiring train: 3 stadi in serie (emb/mesh per stadio), non stream paralleli."
302
- ),
303
- "api": {
304
- "encode_v1": "text → pezzi_ids",
305
- "pass_to_v2": "pezzi_ids → parole_ids (aggrega pezzi)",
306
- "pass_to_v3": "parole surfaces → unioni (2/3 se frequenti)",
307
- "encode_cascade": "encode_v1 → pass_to_v2 → pass_to_v3",
308
- },
309
- }
310
- (out / "manifest.json").write_text(
311
- json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8"
312
- )
313
- bundle.root = out
314
- return out
315
-
316
-
317
- def load_bundle(root: Path | str) -> StageBundle:
318
- root = Path(root)
319
- pezzi = load_stage_vocab(root / "00_pezzi_by_id.txt", "pezzi")
320
- parole = load_stage_vocab(root / "01_parole_by_id.txt", "parole")
321
- unioni = load_stage_vocab(root / "02_unioni_by_id.txt", "unioni")
322
- return StageBundle(pezzi=pezzi, parole=parole, unioni=unioni, root=root)
323
-
324
-
325
- # ---------------------------------------------------------------------------
326
- # V1 — spezzetta tutto in pezzi
327
- # ---------------------------------------------------------------------------
328
-
329
-
330
- def encode_pezzi_fallback(text: str, pezzi: StageVocab) -> list[int]:
331
- """Encode grezzo a pezzi: match longest token goTommy2, altrimenti char/UNK.
332
-
333
- Case-sensitive prima (A≠a nel vocab pezzi); lower solo se manca il match.
334
- Whitespace distinto come gotommy: \\n→<riga>, \\t→<tab>, spazi→<spazio>.
335
- """
336
- s = text
337
- ids: list[int] = []
338
- i = 0
339
- max_len = max((len(t) for t in pezzi.id_to_token if not t.startswith("<")), default=1)
340
- while i < len(s):
341
- ch = s[i]
342
- if ch.isspace():
343
- # Distingue whitespace: allineato a gotommy_encode / token speciali pezzi.
344
- if ch == "\n":
345
- ws_tok = "<riga>"
346
- elif ch == "\t":
347
- ws_tok = "<tab>"
348
- elif ch == "\r":
349
- ws_tok = "<cr>"
350
- elif ch == "\xa0":
351
- ws_tok = "<nbsp>"
352
- else:
353
- ws_tok = "<spazio>"
354
- tid = pezzi.token_to_id.get(ws_tok)
355
- if tid is not None:
356
- ids.append(tid)
357
- i += 1
358
- continue
359
- matched = False
360
- upper = min(len(s), i + max_len)
361
- for j in range(upper, i, -1):
362
- frag = s[i:j]
363
- # Lookup case-sensitive PRIMA; lower solo fallback (es. "Hello"→H non h).
364
- tid = pezzi.token_to_id.get(frag)
365
- if tid is None and frag.isalpha():
366
- tid = pezzi.token_to_id.get(frag.lower())
367
- if tid is not None:
368
- ids.append(tid)
369
- i = j
370
- matched = True
371
- break
372
- if matched:
373
- continue
374
- tid = pezzi.token_to_id.get(ch)
375
- if tid is None and ch.isalpha():
376
- tid = pezzi.token_to_id.get(ch.lower())
377
- if tid is None:
378
- tid = pezzi.unk_id
379
- ids.append(tid)
380
- i += 1
381
- return ids
382
-
383
-
384
- def encode_v1(text: str, pezzi: StageVocab) -> V1Out:
385
- """Vocab 1: spezzetta TUTTO il testo in pezzi/token base (~1200).
386
-
387
- Preferisce gotommy_encode (tokenizer ufficiale goTommy2); fallback locale se
388
- gptommy non è importabile o fallisce.
389
- """
390
- ids: list[int] | None = None
391
- try:
392
- from gptommy.encode import encode as gotommy_encode
393
-
394
- ids = gotommy_encode(text, pezzi)
395
- except Exception:
396
- ids = None
397
- if ids is None:
398
- ids = encode_pezzi_fallback(text, pezzi)
399
- surfaces = [
400
- pezzi.id_to_token[i] if 0 <= i < len(pezzi.id_to_token) else "<|unk|>" for i in ids
401
- ]
402
- return V1Out(pezzi_ids=ids, surfaces=surfaces)
403
-
404
-
405
- def _is_word_break_surface(surf: str) -> bool:
406
- if not surf or surf.startswith("<|"):
407
- # specials di controllo: non rompono da soli (trattati come pezzi “vivi”
408
- # solo se non in _WORD_BREAK); <|unk|> resta nel gruppo parola
409
- return surf in _WORD_BREAK_PEZZI or surf == "<spazio>"
410
- if surf in _WORD_BREAK_PEZZI:
411
- return True
412
- # singolo char di punteggiatura
413
- if len(surf) == 1 and not surf.isalnum() and not surf.isalpha():
414
- return True
415
- return False
416
-
417
-
418
- def _surface_from_pezzo(surf: str) -> str:
419
- """Contributo testuale di un pezzo alla parola ricostruita (salta meta-token)."""
420
- if not surf or surf.startswith("<"):
421
- return ""
422
- return surf
423
-
424
-
425
- # ---------------------------------------------------------------------------
426
- # V2 — aggrega pezzi → parole
427
- # ---------------------------------------------------------------------------
428
-
429
-
430
- def reconstruct_words_from_pezzi(
431
- pezzi_ids: Sequence[int],
432
- pezzi: StageVocab,
433
- ) -> tuple[list[str], list[list[int]]]:
434
- """Da sequenza pezzi → liste di superfici parola + gruppi pezzi_ids.
435
-
436
- Split su spazio/punteggiatura; concatena le superfici dei pezzi nel gruppo.
437
- Usato sia in build (conta parole da V1) sia in pass_to_v2.
438
- """
439
- surfaces: list[str] = []
440
- groups: list[list[int]] = []
441
- buf_surfs: list[str] = []
442
- buf_ids: list[int] = []
443
-
444
- def _flush() -> None:
445
- nonlocal buf_surfs, buf_ids
446
- if not buf_ids:
447
- buf_surfs = []
448
- return
449
- word = "".join(buf_surfs).lower()
450
- if word:
451
- surfaces.append(word)
452
- groups.append(list(buf_ids))
453
- buf_surfs = []
454
- buf_ids = []
455
-
456
- for pid in pezzi_ids:
457
- surf = pezzi.id_to_token[pid] if 0 <= pid < len(pezzi.id_to_token) else "<|unk|>"
458
- if _is_word_break_surface(surf):
459
- _flush()
460
- continue
461
- piece = _surface_from_pezzo(surf)
462
- if piece:
463
- buf_surfs.append(piece)
464
- buf_ids.append(int(pid))
465
- else:
466
- # meta-token senza contributo testuale: chiudi la parola corrente
467
- _flush()
468
- _flush()
469
- return surfaces, groups
470
-
471
-
472
- def pass_to_v2(pezzi_ids: Sequence[int], bundle: StageBundle) -> V2Out:
473
- """Vocab 2: prende i pezzi di V1, aggrega/modifica in parole (id space parole)."""
474
- word_surfs, groups = reconstruct_words_from_pezzi(pezzi_ids, bundle.pezzi)
475
- parole_ids: list[int] = []
476
- for w in word_surfs:
477
- wid = bundle.parole.token_to_id.get(w)
478
- if wid is None:
479
- wid = bundle.parole.unk_id
480
- parole_ids.append(wid)
481
- return V2Out(parole_ids=parole_ids, surfaces=word_surfs, pezzi_groups=groups)
482
-
483
-
484
- # ---------------------------------------------------------------------------
485
- # V3 — unioni 2/3 parole sull'uscita V2
486
- # ---------------------------------------------------------------------------
487
-
488
-
489
- def pass_to_v3(
490
- v2: Union[V2Out, Sequence[str], Sequence[int]],
491
- bundle: StageBundle,
492
- ) -> V3Out:
493
- """Vocab 3: sull'uscita di V2, unisce 2/3 parole se presenti in vocab unioni.
494
-
495
- Merge greedy sinistra→destra: trigram poi bigram; altrimenti passa la parola.
496
- """
497
- if isinstance(v2, V2Out):
498
- surfaces = list(v2.surfaces)
499
- parole_ids = list(v2.parole_ids)
500
- elif v2 and isinstance(v2[0], int): # type: ignore[index]
501
- parole_ids = [int(x) for x in v2] # type: ignore[arg-type]
502
- surfaces = [
503
- bundle.parole.id_to_token[i]
504
- if 0 <= i < len(bundle.parole.id_to_token)
505
- else "<|unk|>"
506
- for i in parole_ids
507
- ]
508
- else:
509
- surfaces = [str(x) for x in v2] # type: ignore[arg-type]
510
- parole_ids = [
511
- bundle.parole.token_to_id.get(s, bundle.parole.unk_id) for s in surfaces
512
- ]
513
-
514
- unioni_ids = [-1] * len(surfaces)
515
- stream: list[tuple[str, str, int]] = []
516
- out_surfaces: list[str] = []
517
- i = 0
518
- while i < len(surfaces):
519
- if i + 2 < len(surfaces):
520
- tri = f"{surfaces[i]} {surfaces[i + 1]} {surfaces[i + 2]}"
521
- uid = bundle.unioni.token_to_id.get(tri)
522
- if uid is not None:
523
- unioni_ids[i] = uid
524
- stream.append(("unione", tri, uid))
525
- out_surfaces.append(tri)
526
- i += 3
527
- continue
528
- if i + 1 < len(surfaces):
529
- bi = f"{surfaces[i]} {surfaces[i + 1]}"
530
- uid = bundle.unioni.token_to_id.get(bi)
531
- if uid is not None:
532
- unioni_ids[i] = uid
533
- stream.append(("unione", bi, uid))
534
- out_surfaces.append(bi)
535
- i += 2
536
- continue
537
- wid = parole_ids[i] if i < len(parole_ids) else bundle.parole.unk_id
538
- stream.append(("parola", surfaces[i], wid))
539
- out_surfaces.append(surfaces[i])
540
- i += 1
541
-
542
- return V3Out(unioni_ids=unioni_ids, stream=stream, surfaces=out_surfaces)
543
-
544
-
545
- # ---------------------------------------------------------------------------
546
- # Cascade + compat
547
- # ---------------------------------------------------------------------------
548
-
549
-
550
- def encode_cascade(text: str, bundle: StageBundle) -> StageEncoded:
551
- """Pipeline ufficiale: testo → encode_v1 → pass_to_v2 → pass_to_v3."""
552
- v1 = encode_v1(text, bundle.pezzi)
553
- v2 = pass_to_v2(v1.pezzi_ids, bundle)
554
- v3 = pass_to_v3(v2, bundle)
555
- spans: list[tuple[str, str, object]] = []
556
- for s, pid in zip(v1.surfaces, v1.pezzi_ids):
557
- spans.append(("v1", s, pid))
558
- for s, wid, grp in zip(v2.surfaces, v2.parole_ids, v2.pezzi_groups):
559
- spans.append(("v2", s, (wid, grp)))
560
- for kind, surf, tid in v3.stream:
561
- spans.append(("v3", f"{kind}:{surf}", tid))
562
- return StageEncoded(
563
- pezzi_ids=v1.pezzi_ids,
564
- parole_ids=v2.parole_ids,
565
- unioni_ids=v3.unioni_ids,
566
- spans=spans,
567
- v1=v1,
568
- v2=v2,
569
- v3=v3,
570
- )
571
-
572
-
573
- def _word_union_ids_from_v3(v3: V3Out, n_words: int) -> list[int]:
574
- """Id unione per parola V2: solo sull'ULTIMA parola di ogni unione (causale).
575
-
576
- Non propagare l'uid sulle parole iniziali/intermedie dell'unione: sarebbe
577
- lookahead (si rivelerebbe il merge prima che le parole successive siano
578
- complete). Parole senza unione → -1.
579
- """
580
- out = [-1] * n_words
581
- wi = 0
582
- for kind, surf, tid in v3.stream:
583
- if kind == "unione":
584
- n = max(1, len(surf.split()))
585
- last = wi + n - 1
586
- if 0 <= last < n_words:
587
- out[last] = int(tid)
588
- wi += n
589
- else:
590
- wi += 1
591
- return out
592
-
593
-
594
- def _align_anchor_for_word(
595
- pezzi: Sequence[int],
596
- pezzi_vocab: StageVocab,
597
- first_t: int,
598
- last_t: int,
599
- ) -> int:
600
- """Ancora causale per una parola: spazio/break successivo, altrimenti ultimo pezzo.
601
-
602
- Mai il primo pezzo se la parola è multi-pezzo (anti-lookahead).
603
- """
604
- n = len(pezzi)
605
- anchor = last_t
606
- if last_t + 1 < n:
607
- nxt = (
608
- pezzi_vocab.id_to_token[pezzi[last_t + 1]]
609
- if 0 <= pezzi[last_t + 1] < len(pezzi_vocab.id_to_token)
610
- else "<|unk|>"
611
- )
612
- if _is_word_break_surface(nxt):
613
- anchor = last_t + 1
614
- # sicurezza: multi-pezzo non deve mai ancorare sul primo
615
- if first_t != last_t and anchor == first_t:
616
- anchor = last_t
617
- return anchor
618
-
619
-
620
- def _carry_forward_after_anchor(ids: list[int], pad_id: int) -> None:
621
- """In-place: dopo un id non-pad, copia l'ultimo visto sui pad successivi.
622
-
623
- Non tocca il prefisso di soli pad (anti-lookahead pre-ancora resta valido).
624
- """
625
- last = pad_id
626
- for i, v in enumerate(ids):
627
- if v != pad_id:
628
- last = int(v)
629
- elif last != pad_id:
630
- ids[i] = last
631
-
632
-
633
- def _is_word_terminator(surf: str) -> bool:
634
- """Pezzo che chiude la parola in V2: break oppure pezzo senza testo (<riga>, <tab>, <|unk|>, …)."""
635
- return _is_word_break_surface(surf) or not _surface_from_pezzo(surf)
636
-
637
-
638
- def _prefix_union_ids(surfaces: Sequence[str], bundle: StageBundle) -> list[int]:
639
- """Per ogni parola j: ultima unione di ``pass_to_v3`` sulle sole parole 0..j (-1 se nessuna).
640
-
641
- Il merge greedy decide in i guardando le parole i+1 e i+2: le decisioni con
642
- i <= j-2 coincidono con quelle sull'intera sequenza, la coda del prefisso è
643
- al più il bigramma (j-1, j).
644
- """
645
- uni = bundle.unioni.token_to_id
646
- n = len(surfaces)
647
- starts: list[int] = []
648
- uids: list[int] = []
649
- i = 0
650
- while i < n:
651
- size, uid = 1, None
652
- if i + 2 < n:
653
- uid = uni.get(f"{surfaces[i]} {surfaces[i + 1]} {surfaces[i + 2]}")
654
- size = 3
655
- if uid is None and i + 1 < n:
656
- uid = uni.get(f"{surfaces[i]} {surfaces[i + 1]}")
657
- size = 2
658
- if uid is None:
659
- size = 1
660
- starts.append(i)
661
- uids.append(-1 if uid is None else int(uid))
662
- i += size
663
-
664
- out = [-1] * n
665
- k = 0
666
- last = -1
667
- for j in range(n):
668
- while k < len(starts) and starts[k] <= j - 2:
669
- if uids[k] >= 0:
670
- last = uids[k]
671
- k += 1
672
- cur = last
673
- if k < len(starts) and starts[k] == j - 1:
674
- uid = uni.get(f"{surfaces[j - 1]} {surfaces[j]}")
675
- if uid is not None:
676
- cur = int(uid)
677
- out[j] = cur
678
- return out
679
-
680
-
681
- def _align_causal(enc: StageEncoded, bundle: StageBundle) -> tuple[list[int], list[int], list[int]]:
682
- """Allineamento in cui la feature in t dipende solo da pezzi[:t+1] (vedi align_stages_to_pezzi)."""
683
- pezzi = list(enc.pezzi_ids)
684
- n = len(pezzi)
685
- pad_w = bundle.parole.pad_id
686
- pad_u = bundle.unioni.pad_id
687
- parole_al = [pad_w] * n
688
- unioni_al = [pad_u] * n
689
- if n == 0:
690
- return pezzi, parole_al, unioni_al
691
- pv = bundle.pezzi
692
- word_last: list[int] = []
693
- in_word = False
694
- for t, pid in enumerate(pezzi):
695
- surf = pv.id_to_token[pid] if 0 <= pid < len(pv.id_to_token) else "<|unk|>"
696
- if _is_word_terminator(surf):
697
- in_word = False
698
- continue
699
- if not in_word:
700
- word_last.append(t)
701
- in_word = True
702
- word_last[-1] = t
703
- if enc.v2 is not None:
704
- surfaces = list(enc.v2.surfaces)
705
- else:
706
- surfaces, _ = reconstruct_words_from_pezzi(pezzi, pv)
707
- if len(surfaces) != len(word_last) or len(enc.parole_ids) != len(word_last):
708
- raise ValueError("align causale: parole V2 e gruppi di pezzi non coincidono")
709
- word_uids = _prefix_union_ids(surfaces, bundle)
710
- for w, last_t in enumerate(word_last):
711
- anchor = last_t + 1
712
- if anchor >= n:
713
- continue
714
- parole_al[anchor] = int(enc.parole_ids[w])
715
- if word_uids[w] >= 0:
716
- unioni_al[anchor] = word_uids[w]
717
- _carry_forward_after_anchor(parole_al, pad_w)
718
- _carry_forward_after_anchor(unioni_al, pad_u)
719
- return pezzi, parole_al, unioni_al
720
-
721
-
722
- def align_stages_to_pezzi(
723
- enc: StageEncoded,
724
- bundle: StageBundle,
725
- causal: bool = False,
726
- ) -> tuple[list[int], list[int], list[int]]:
727
- """Allinea parole/unioni alla timeline pezzi (stessa lunghezza).
728
-
729
- ``causal=True`` (``--causal-align``): la feature in t è quella calcolabile
730
- dai soli pezzi[:t+1]. Rispetto alla policy qui sotto cambia in tre punti:
731
- - l'ancora di una parola è sempre il pezzo che la chiude in V2 (break
732
- oppure pezzo senza testo: <riga>, <tab>, <cr>, <nbsp>, <|unk|>, …),
733
- mai il suo ultimo pezzo: finché non arriva il terminatore la parola
734
- potrebbe continuare;
735
- - una parola non chiusa a fine sequenza non viene mai rivelata;
736
- - l'unione mostrata sull'ancora della parola j è l'ultima unione del
737
- merge V3 sulle sole parole 0..j (il greedy trigramma→bigramma
738
- sull'intera sequenza guarda due parole avanti).
739
-
740
- Policy (due pass):
741
-
742
- 1) Ancora CAUSALE (anti-lookahead; coerente con gen greedy che mette
743
- pad_parole/pad_unioni sui pezzi nuovi finché la parola non è chiusa):
744
-
745
- - Non assegnare id parola/unione al PRIMO pezzo di una parola multi-pezzo
746
- (leak: rivelerebbe l'identità prima di aver visto tutti i pezzi).
747
- - Id parola: solo sull'ultimo pezzo della parola, oppure sullo
748
- spazio/break immediatamente successivo se presente.
749
- - Id unione: solo sull'ancora dell'ultima parola coperta dall'unione
750
- (vedi `_word_union_ids_from_v3`).
751
- - Pezzi intermedi / primo pezzo (multi-pezzo) / break non-ancora →
752
- ``pad`` di parole/unioni. Non si usa ``unk`` qui: unk = superficie
753
- assente dal vocab V2/V3, non “non ancora nota”.
754
- - Parola mono-pezzo senza break successivo: ancora = quel pezzo
755
- (superficie già completa → non è lookahead).
756
-
757
- 2) Carry-forward post-ancora: una volta vista un'ancora, l'ultima
758
- parola/unione nota si propaga alle posizioni successive ancora in
759
- ``pad`` (spazi, pezzi della parola successiva, punteggiatura extra),
760
- finché non arriva una nuova ancora. Così su ``"Answer: "`` lo stadio
761
- V2 vede ``answer`` anche sullo ``<spazio>`` finale, non un reset a pad.
762
- Il prefisso pre-prima-ancora resta pad (niente leak sul primo pezzo).
763
- """
764
- if causal:
765
- return _align_causal(enc, bundle)
766
- pezzi = list(enc.pezzi_ids)
767
- n = len(pezzi)
768
- pad_w = bundle.parole.pad_id
769
- pad_u = bundle.unioni.pad_id
770
- parole_al = [pad_w] * n
771
- unioni_al = [pad_u] * n
772
- if n == 0:
773
- return pezzi, parole_al, unioni_al
774
-
775
- pezzi_vocab = bundle.pezzi
776
- n_words = len(enc.parole_ids)
777
- word_first = [-1] * n_words
778
- word_last = [-1] * n_words
779
- word_idx = -1
780
- in_word = False
781
- for t, pid in enumerate(pezzi):
782
- surf = (
783
- pezzi_vocab.id_to_token[pid]
784
- if 0 <= pid < len(pezzi_vocab.id_to_token)
785
- else "<|unk|>"
786
- )
787
- if _is_word_break_surface(surf):
788
- in_word = False
789
- continue
790
- piece = _surface_from_pezzo(surf)
791
- if piece:
792
- if not in_word:
793
- word_idx += 1
794
- in_word = True
795
- if 0 <= word_idx < n_words:
796
- word_first[word_idx] = t
797
- if 0 <= word_idx < n_words:
798
- word_last[word_idx] = t
799
- else:
800
- in_word = False
801
-
802
- if enc.v3 is not None:
803
- word_uids = _word_union_ids_from_v3(enc.v3, n_words)
804
- else:
805
- word_uids = [-1] * n_words
806
- # fallback senza stream: unioni_ids è start-aligned in V3Out;
807
- # senza n-gram non possiamo spostare sull'ultima parola.
808
- for i, uid in enumerate(enc.unioni_ids):
809
- if 0 <= i < n_words and int(uid) >= 0:
810
- word_uids[i] = int(uid)
811
-
812
- for w in range(n_words):
813
- first_t = word_first[w]
814
- last_t = word_last[w]
815
- if first_t < 0 or last_t < 0:
816
- continue
817
- anchor = _align_anchor_for_word(pezzi, pezzi_vocab, first_t, last_t)
818
- parole_al[anchor] = int(enc.parole_ids[w])
819
- uid = word_uids[w] if w < len(word_uids) else -1
820
- if uid >= 0:
821
- unioni_al[anchor] = int(uid)
822
-
823
- _carry_forward_after_anchor(parole_al, pad_w)
824
- _carry_forward_after_anchor(unioni_al, pad_u)
825
- return pezzi, parole_al, unioni_al
826
-
827
-
828
- def encode_stages(text: str, bundle: StageBundle) -> StageEncoded:
829
- """DEPRECATED: ex greedy parallelo; ora alias di encode_cascade (V1→V2→V3)."""
830
- warnings.warn(
831
- "encode_stages greedy è deprecato; usa encode_cascade / encode_v1→pass_to_v2→pass_to_v3",
832
- DeprecationWarning,
833
- stacklevel=2,
834
- )
835
- return encode_cascade(text, bundle)
836
-
837
-
838
- # ---------------------------------------------------------------------------
839
- # Build: vocab2 da pezzi (V1), vocab3 da uscita V2
840
- # ---------------------------------------------------------------------------
841
-
842
-
843
- def iter_v1_word_surfaces(
844
- texts: Iterator[str],
845
- pezzi: StageVocab,
846
- *,
847
- max_chars: int = 0,
848
- ) -> Iterator[list[str]]:
849
- """Per ogni testo: V1 → parole ricostruite da pezzi (senza ancora Vocab 2)."""
850
- seen = 0
851
- for text in texts:
852
- if max_chars > 0 and seen >= max_chars:
853
- break
854
- seen += len(text)
855
- v1 = encode_v1(text, pezzi)
856
- surfs, _ = reconstruct_words_from_pezzi(v1.pezzi_ids, pezzi)
857
- if surfs:
858
- yield surfs
859
-
860
-
861
- def count_from_cascade(
862
- texts: Iterator[str],
863
- pezzi: StageVocab,
864
- *,
865
- max_chars: int = 0,
866
- ) -> tuple[dict[str, int], dict[str, int], dict[str, int]]:
867
- """Conta parole (da V1→ricostruzione) e bi/tri sulla stessa sequenza parole.
868
-
869
- Nota build: vocab3 si impara dalle unioni sulle superfici che V2 vedrà
870
- (parole ricostruite da pezzi). Dopo aver creato Vocab 2 si può rivalidare
871
- con pass_to_v2; le superfici coincidono.
872
- """
873
- uni: Counter[str] = Counter()
874
- bi: Counter[str] = Counter()
875
- tri: Counter[str] = Counter()
876
- for surfs in iter_v1_word_surfaces(texts, pezzi, max_chars=max_chars):
877
- uni.update(surfs)
878
- if len(surfs) >= 2:
879
- bi.update(f"{surfs[i]} {surfs[i + 1]}" for i in range(len(surfs) - 1))
880
- if len(surfs) >= 3:
881
- tri.update(
882
- f"{surfs[i]} {surfs[i + 1]} {surfs[i + 2]}" for i in range(len(surfs) - 2)
883
- )
884
- return dict(uni), dict(bi), dict(tri)
885
-
886
-
887
- def count_ngrams_from_texts(
888
- texts: Iterator[str],
889
- *,
890
- max_chars: int = 0,
891
- pezzi: StageVocab | None = None,
892
- ) -> tuple[dict[str, int], dict[str, int], dict[str, int]]:
893
- """Build helper: conta via cascade V1→parole ricostruite (non word-tokenize grezzo)."""
894
- if pezzi is None:
895
- pezzi = load_pezzi_vocab()
896
- return count_from_cascade(texts, pezzi, max_chars=max_chars)
897
-
898
-
899
- def build_stages_from_counts(
900
- word_counts: dict[str, int],
901
- bigram_counts: dict[str, int],
902
- trigram_counts: dict[str, int],
903
- *,
904
- pezzi_path: Path | str | None = None,
905
- min_word_freq: int = 3,
906
- min_union_freq: int = 5,
907
- max_words: int = 50000,
908
- max_unions: int = 20000,
909
- out_dir: Path | str | None = None,
910
- ) -> StageBundle:
911
- """Costruisce V1 (carica pezzi), V2 da conteggi parole-da-pezzi, V3 da unioni su V2."""
912
- pezzi = load_pezzi_vocab(pezzi_path)
913
- ranked_words = sorted(
914
- ((w, c) for w, c in word_counts.items() if c >= min_word_freq and len(w) > 1),
915
- key=lambda x: (-x[1], x[0]),
916
- )[:max_words]
917
- parole = make_stage_vocab(
918
- "parole",
919
- (w for w, _ in ranked_words),
920
- meta={
921
- "min_word_freq": min_word_freq,
922
- "n_candidates": len(ranked_words),
923
- "source": "reconstructed_from_v1_pezzi",
924
- "top": ranked_words[:20],
925
- },
926
- )
927
-
928
- union_items: list[tuple[str, int, str]] = []
929
- for t, c in trigram_counts.items():
930
- if c >= min_union_freq:
931
- union_items.append((t, c, "tri"))
932
- for t, c in bigram_counts.items():
933
- if c >= min_union_freq:
934
- union_items.append((t, c, "bi"))
935
- union_items.sort(key=lambda x: (-x[1], 0 if x[2] == "tri" else 1, x[0]))
936
- union_items = union_items[:max_unions]
937
- unioni = make_stage_vocab(
938
- "unioni",
939
- (t for t, _, _ in union_items),
940
- meta={
941
- "min_union_freq": min_union_freq,
942
- "n_candidates": len(union_items),
943
- "source": "frequent_unions_on_v2_surfaces",
944
- "n_trigram": sum(1 for _, _, k in union_items if k == "tri"),
945
- "n_bigram": sum(1 for _, _, k in union_items if k == "bi"),
946
- "top": [(t, c, k) for t, c, k in union_items[:20]],
947
- },
948
- )
949
-
950
- root = Path(out_dir) if out_dir is not None else DEFAULT_OUT
951
- bundle = StageBundle(pezzi=pezzi, parole=parole, unioni=unioni, root=root)
952
- save_bundle(bundle, root)
953
- counts_path = root / "counts_summary.json"
954
- counts_path.write_text(
955
- json.dumps(
956
- {
957
- "pipeline": "v1_pezzi → v2_parole(from pezzi) → v3_unioni(from v2)",
958
- "n_word_types": len(word_counts),
959
- "n_bigram_types": len(bigram_counts),
960
- "n_trigram_types": len(trigram_counts),
961
- "parole_kept": len(parole) - len(SPECIALS),
962
- "unioni_kept": len(unioni) - len(SPECIALS),
963
- "min_word_freq": min_word_freq,
964
- "min_union_freq": min_union_freq,
965
- },
966
- ensure_ascii=False,
967
- indent=2,
968
- ),
969
- encoding="utf-8",
970
- )
971
- return bundle
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vocab_stages/00_pezzi_by_id.meta.json DELETED
@@ -1,17 +0,0 @@
1
- {
2
- "name": "pezzi",
3
- "size": 1251,
4
- "path": "/Users/thomasseri/Desktop/columns/geodesic_mesh_nn/vocab_stages/00_pezzi_by_id.txt",
5
- "meta": {
6
- "source": "/Users/thomasseri/Desktop/columns/logs/goTommy2_vocab_by_id.txt",
7
- "note": "goTommy2-scale ~1200 (1251)"
8
- },
9
- "specials": [
10
- "<|end|>",
11
- "<|bos|>",
12
- "<|eos|>",
13
- "<|pad|>",
14
- "<|unk|>",
15
- "<|sep|>"
16
- ]
17
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vocab_stages/00_pezzi_by_id.txt DELETED
@@ -1,1253 +0,0 @@
1
- # stage=pezzi size=1251
2
- # colonne: codice\tscheda\triga\tid\tpezzo
3
- 00000 00 000 0 <|end|>
4
- 00001 00 001 1 <|bos|>
5
- 00002 00 002 2 <|eos|>
6
- 00003 00 003 3 <|pad|>
7
- 00004 00 004 4 <|unk|>
8
- 00005 00 005 5 <|sep|>
9
- 00006 00 006 6 <|mask|>
10
- 00007 00 007 7 <|cls|>
11
- 00008 00 008 8 <|stop|>
12
- 00009 00 009 9 <|break|>
13
- 00010 00 010 10 <spazio>
14
- 00011 00 011 11 <riga>
15
- 00012 00 012 12 <tab>
16
- 00013 00 013 13 <cr>
17
- 00014 00 014 14 <nbsp>
18
- 00015 00 015 15 <|system|>
19
- 00016 00 016 16 <|user|>
20
- 00017 00 017 17 <|assistant|>
21
- 00018 00 018 18 <|tu|>
22
- 00019 00 019 19 <|ai|>
23
- 00020 00 020 20 <|tool|>
24
- 00021 00 021 21 <|tool_result|>
25
- 00022 00 022 22 <|im_start|>
26
- 00023 00 023 23 <|im_end|>
27
- 00024 00 024 24 <|db|>
28
- 00025 00 025 25 <|scheda|>
29
- 00026 00 026 26 <|indice|>
30
- 00027 00 027 27 <|sposta|>
31
- 00028 00 028 28 <|correggi|>
32
- 00029 00 029 29 <|aggiungi|>
33
- 00030 00 030 30 <|vuoto|>
34
- 00031 00 031 31 <|think|>
35
- 00032 00 032 32 <|/think|>
36
- 00033 00 033 33 <|appunti|>
37
- 00034 00 034 34 <|fiducia|>
38
- 00035 00 035 35 <|chiedo|>
39
- 00036 00 036 36 <|capito|>
40
- 00037 00 037 37 <|colonna|>
41
- 00038 00 038 38 <|hub|>
42
- 00039 00 039 39 <|cerca|>
43
- 00040 00 040 40 <|fonte|>
44
- 00041 00 041 41 <|cita|>
45
- 00042 00 042 42 <|no_fonte|>
46
- 00043 00 043 43 <|code|>
47
- 00044 00 044 44 <|/code|>
48
- 00045 00 045 45 <|json|>
49
- 00046 00 046 46 <|lista|>
50
- 00047 00 047 47 <|tabella|>
51
- 00048 00 048 48 <|math|>
52
- 00049 00 049 49 <|url|>
53
- 00050 00 050 50 <|non_so|>
54
- 00051 00 051 51 <|esempio|>
55
- 00052 00 052 52 <|attenzione|>
56
- 00053 00 053 53 0
57
- 00054 00 054 54 1
58
- 00055 00 055 55 2
59
- 00056 00 056 56 3
60
- 00057 00 057 57 4
61
- 00058 00 058 58 5
62
- 00059 00 059 59 6
63
- 00060 00 060 60 7
64
- 00061 00 061 61 8
65
- 00062 00 062 62 9
66
- 00063 00 063 63 .
67
- 00064 00 064 64 ,
68
- 00065 00 065 65 ;
69
- 00066 00 066 66 :
70
- 00067 00 067 67 !
71
- 00068 00 068 68 ?
72
- 00069 00 069 69 …
73
- 00070 00 070 70 ·
74
- 00071 00 071 71 '
75
- 00072 00 072 72 "
76
- 00073 00 073 73 «
77
- 00074 00 074 74 »
78
- 00075 00 075 75 (
79
- 00076 00 076 76 )
80
- 00077 00 077 77 [
81
- 00078 00 078 78 ]
82
- 00079 00 079 79 {
83
- 00080 00 080 80 }
84
- 00081 00 081 81 +
85
- 00082 00 082 82 -
86
- 00083 00 083 83 *
87
- 00084 00 084 84 /
88
- 00085 00 085 85 =
89
- 00086 00 086 86 <
90
- 00087 00 087 87 >
91
- 00088 00 088 88 @
92
- 00089 00 089 89 #
93
- 00090 00 090 90 %
94
- 00091 00 091 91 &
95
- 00092 00 092 92 _
96
- 00093 00 093 93 |
97
- 00094 00 094 94 \
98
- 00095 00 095 95 ^
99
- 00096 00 096 96 ~
100
- 00097 00 097 97 `
101
- 00098 00 098 98 $
102
- 00099 00 099 99 Ͱ
103
- 00100 00 100 100 ͱ
104
- 00101 00 101 101 Ͳ
105
- 00102 00 102 102 ͳ
106
- 00103 00 103 103 ʹ
107
- 00104 00 104 104 Ͷ
108
- 00105 00 105 105 ͷ
109
- 00106 00 106 106 ͺ
110
- 00107 00 107 107 ͻ
111
- 00108 00 108 108 ͼ
112
- 00109 00 109 109 ͽ
113
- 00110 00 110 110 Ϳ
114
- 00111 00 111 111 Ά
115
- 00112 00 112 112 Έ
116
- 00113 00 113 113 Ή
117
- 00114 00 114 114 Ί
118
- 00115 00 115 115 Ό
119
- 00116 00 116 116 Ύ
120
- 00117 00 117 117 Ώ
121
- 00118 00 118 118 ΐ
122
- 00119 00 119 119 Α
123
- 00120 00 120 120 Β
124
- 00121 00 121 121 Γ
125
- 00122 00 122 122 Δ
126
- 00123 00 123 123 Ε
127
- 00124 00 124 124 Ζ
128
- 00125 00 125 125 Η
129
- 00126 00 126 126 Θ
130
- 00127 00 127 127 Ι
131
- 00128 00 128 128 Κ
132
- 00129 00 129 129 Λ
133
- 00130 00 130 130 Μ
134
- 00131 00 131 131 Ν
135
- 00132 00 132 132 Ξ
136
- 00133 00 133 133 Ο
137
- 00134 00 134 134 Π
138
- 00135 00 135 135 Ρ
139
- 00136 00 136 136 Σ
140
- 00137 00 137 137 Τ
141
- 00138 00 138 138 Υ
142
- 00139 00 139 139 Φ
143
- 00140 00 140 140 Χ
144
- 00141 00 141 141 Ψ
145
- 00142 00 142 142 Ω
146
- 00143 00 143 143 Ϊ
147
- 00144 00 144 144 Ϋ
148
- 00145 00 145 145 ά
149
- 00146 00 146 146 έ
150
- 00147 00 147 147 ή
151
- 00148 00 148 148 ί
152
- 00149 00 149 149 ΰ
153
- 00150 00 150 150 α
154
- 00151 00 151 151 β
155
- 00152 00 152 152 γ
156
- 00153 00 153 153 δ
157
- 00154 00 154 154 ε
158
- 00155 00 155 155 ζ
159
- 00156 00 156 156 η
160
- 00157 00 157 157 θ
161
- 00158 00 158 158 ι
162
- 00159 00 159 159 κ
163
- 00160 00 160 160 λ
164
- 00161 00 161 161 μ
165
- 00162 00 162 162 ν
166
- 00163 00 163 163 ξ
167
- 00164 00 164 164 ο
168
- 00165 00 165 165 π
169
- 00166 00 166 166 ρ
170
- 00167 00 167 167 ς
171
- 00168 00 168 168 σ
172
- 00169 00 169 169 τ
173
- 00170 00 170 170 υ
174
- 00171 00 171 171 φ
175
- 00172 00 172 172 χ
176
- 00173 00 173 173 ψ
177
- 00174 00 174 174 ω
178
- 00175 00 175 175 ϊ
179
- 00176 00 176 176 ϋ
180
- 00177 00 177 177 ό
181
- 00178 00 178 178 ύ
182
- 00179 00 179 179 ώ
183
- 00180 00 180 180 Ϗ
184
- 00181 00 181 181 ϐ
185
- 00182 00 182 182 ϑ
186
- 00183 00 183 183 ϒ
187
- 00184 00 184 184 ϓ
188
- 00185 00 185 185 ϔ
189
- 00186 00 186 186 ϕ
190
- 00187 00 187 187 ϖ
191
- 00188 00 188 188 ϗ
192
- 00189 00 189 189 Ϙ
193
- 00190 00 190 190 ϙ
194
- 00191 00 191 191 Ϛ
195
- 00192 00 192 192 ϛ
196
- 00193 00 193 193 Ϝ
197
- 00194 00 194 194 ϝ
198
- 00195 00 195 195 Ϟ
199
- 00196 00 196 196 ϟ
200
- 00197 00 197 197 Ϡ
201
- 00198 00 198 198 ϡ
202
- 00199 00 199 199 Ϣ
203
- 00200 00 200 200 ϣ
204
- 00201 00 201 201 Ϥ
205
- 00202 00 202 202 ϥ
206
- 00203 00 203 203 Ϧ
207
- 00204 00 204 204 ϧ
208
- 00205 00 205 205 Ϩ
209
- 00206 00 206 206 ϩ
210
- 00207 00 207 207 Ϫ
211
- 00208 00 208 208 ϫ
212
- 00209 00 209 209 Ϭ
213
- 00210 00 210 210 ϭ
214
- 00211 00 211 211 Ϯ
215
- 00212 00 212 212 ϯ
216
- 00213 00 213 213 ϰ
217
- 00214 00 214 214 ϱ
218
- 00215 00 215 215 ϲ
219
- 00216 00 216 216 ϳ
220
- 00217 00 217 217 ϴ
221
- 00218 00 218 218 ϵ
222
- 00219 00 219 219 Ϸ
223
- 00220 00 220 220 ϸ
224
- 00221 00 221 221 Ϲ
225
- 00222 00 222 222 Ϻ
226
- 00223 00 223 223 ϻ
227
- 00224 00 224 224 ϼ
228
- 00225 00 225 225 Ͻ
229
- 00226 00 226 226 Ͼ
230
- 00227 00 227 227 Ͽ
231
- 00228 00 228 228 Ѐ
232
- 00229 00 229 229 Ё
233
- 00230 00 230 230 Ђ
234
- 00231 00 231 231 Ѓ
235
- 00232 00 232 232 Є
236
- 00233 00 233 233 Ѕ
237
- 00234 00 234 234 І
238
- 00235 00 235 235 Ї
239
- 00236 00 236 236 Ј
240
- 00237 00 237 237 Љ
241
- 00238 00 238 238 Њ
242
- 00239 00 239 239 Ћ
243
- 00240 00 240 240 Ќ
244
- 00241 00 241 241 Ѝ
245
- 00242 00 242 242 Ў
246
- 00243 00 243 243 Џ
247
- 00244 00 244 244 А
248
- 00245 00 245 245 Б
249
- 00246 00 246 246 В
250
- 00247 00 247 247 Г
251
- 00248 00 248 248 Д
252
- 00249 00 249 249 Е
253
- 00250 00 250 250 Ж
254
- 00251 00 251 251 З
255
- 00252 00 252 252 И
256
- 00253 00 253 253 Й
257
- 00254 00 254 254 К
258
- 00255 00 255 255 Л
259
- 00256 00 256 256 М
260
- 00257 00 257 257 Н
261
- 00258 00 258 258 О
262
- 00259 00 259 259 П
263
- 00260 00 260 260 Р
264
- 00261 00 261 261 С
265
- 00262 00 262 262 Т
266
- 00263 00 263 263 У
267
- 00264 00 264 264 Ф
268
- 00265 00 265 265 Х
269
- 00266 00 266 266 Ц
270
- 00267 00 267 267 Ч
271
- 00268 00 268 268 Ш
272
- 00269 00 269 269 Щ
273
- 00270 00 270 270 Ъ
274
- 00271 00 271 271 Ы
275
- 00272 00 272 272 Ь
276
- 00273 00 273 273 Э
277
- 00274 00 274 274 Ю
278
- 00275 00 275 275 Я
279
- 00276 00 276 276 а
280
- 00277 00 277 277 б
281
- 00278 00 278 278 в
282
- 00279 00 279 279 г
283
- 00280 00 280 280 д
284
- 00281 00 281 281 е
285
- 00282 00 282 282 ж
286
- 00283 00 283 283 з
287
- 00284 00 284 284 и
288
- 00285 00 285 285 й
289
- 00286 00 286 286 к
290
- 00287 00 287 287 л
291
- 00288 00 288 288 м
292
- 00289 00 289 289 н
293
- 00290 00 290 290 о
294
- 00291 00 291 291 п
295
- 00292 00 292 292 р
296
- 00293 00 293 293 с
297
- 00294 00 294 294 т
298
- 00295 00 295 295 у
299
- 00296 00 296 296 ф
300
- 00297 00 297 297 х
301
- 00298 00 298 298 ц
302
- 00299 00 299 299 ч
303
- 00300 00 300 300 ш
304
- 00301 00 301 301 щ
305
- 00302 00 302 302 ъ
306
- 00303 00 303 303 ы
307
- 00304 00 304 304 ь
308
- 00305 00 305 305 э
309
- 00306 00 306 306 ю
310
- 00307 00 307 307 я
311
- 00308 00 308 308 ѐ
312
- 00309 00 309 309 ё
313
- 00310 00 310 310 ђ
314
- 00311 00 311 311 ѓ
315
- 00312 00 312 312 є
316
- 00313 00 313 313 ѕ
317
- 00314 00 314 314 і
318
- 00315 00 315 315 ї
319
- 00316 00 316 316 ј
320
- 00317 00 317 317 љ
321
- 00318 00 318 318 њ
322
- 00319 00 319 319 ћ
323
- 00320 00 320 320 ќ
324
- 00321 00 321 321 ѝ
325
- 00322 00 322 322 ў
326
- 00323 00 323 323 џ
327
- 00324 00 324 324 Ѡ
328
- 00325 00 325 325 ѡ
329
- 00326 00 326 326 Ѣ
330
- 00327 00 327 327 ѣ
331
- 00328 00 328 328 Ѥ
332
- 00329 00 329 329 ѥ
333
- 00330 00 330 330 Ѧ
334
- 00331 00 331 331 ѧ
335
- 00332 00 332 332 Ѩ
336
- 00333 00 333 333 ѩ
337
- 00334 00 334 334 Ѫ
338
- 00335 00 335 335 ѫ
339
- 00336 00 336 336 Ѭ
340
- 00337 00 337 337 ѭ
341
- 00338 00 338 338 Ѯ
342
- 00339 00 339 339 ѯ
343
- 00340 00 340 340 Ѱ
344
- 00341 00 341 341 ѱ
345
- 00342 00 342 342 Ѳ
346
- 00343 00 343 343 ѳ
347
- 00344 00 344 344 Ѵ
348
- 00345 00 345 345 ѵ
349
- 00346 00 346 346 Ѷ
350
- 00347 00 347 347 ѷ
351
- 00348 00 348 348 Ѹ
352
- 00349 00 349 349 ѹ
353
- 00350 00 350 350 Ѻ
354
- 00351 00 351 351 ѻ
355
- 00352 00 352 352 Ѽ
356
- 00353 00 353 353 ѽ
357
- 00354 00 354 354 Ѿ
358
- 00355 00 355 355 ѿ
359
- 00356 00 356 356 Ҁ
360
- 00357 00 357 357 ҁ
361
- 00358 00 358 358 Ҋ
362
- 00359 00 359 359 ҋ
363
- 00360 00 360 360 Ҍ
364
- 00361 00 361 361 ҍ
365
- 00362 00 362 362 Ҏ
366
- 00363 00 363 363 ҏ
367
- 00364 00 364 364 Ґ
368
- 00365 00 365 365 ґ
369
- 00366 00 366 366 Ғ
370
- 00367 00 367 367 ғ
371
- 00368 00 368 368 Ҕ
372
- 00369 00 369 369 ҕ
373
- 00370 00 370 370 Җ
374
- 00371 00 371 371 җ
375
- 00372 00 372 372 Ҙ
376
- 00373 00 373 373 ҙ
377
- 00374 00 374 374 Қ
378
- 00375 00 375 375 қ
379
- 00376 00 376 376 Ҝ
380
- 00377 00 377 377 ҝ
381
- 00378 00 378 378 Ҟ
382
- 00379 00 379 379 ҟ
383
- 00380 00 380 380 Ҡ
384
- 00381 00 381 381 ҡ
385
- 00382 00 382 382 Ң
386
- 00383 00 383 383 ң
387
- 00384 00 384 384 Ҥ
388
- 00385 00 385 385 ҥ
389
- 00386 00 386 386 Ҧ
390
- 00387 00 387 387 ҧ
391
- 00388 00 388 388 Ҩ
392
- 00389 00 389 389 ҩ
393
- 00390 00 390 390 Ҫ
394
- 00391 00 391 391 ҫ
395
- 00392 00 392 392 Ҭ
396
- 00393 00 393 393 ҭ
397
- 00394 00 394 394 Ү
398
- 00395 00 395 395 ү
399
- 00396 00 396 396 Ұ
400
- 00397 00 397 397 ұ
401
- 00398 00 398 398 Ҳ
402
- 00399 00 399 399 ҳ
403
- 00400 00 400 400 Ҵ
404
- 00401 00 401 401 ҵ
405
- 00402 00 402 402 Ҷ
406
- 00403 00 403 403 ҷ
407
- 00404 00 404 404 Ҹ
408
- 00405 00 405 405 ҹ
409
- 00406 00 406 406 Һ
410
- 00407 00 407 407 һ
411
- 00408 00 408 408 Ҽ
412
- 00409 00 409 409 ҽ
413
- 00410 00 410 410 Ҿ
414
- 00411 00 411 411 ҿ
415
- 00412 00 412 412 Ӏ
416
- 00413 00 413 413 Ӂ
417
- 00414 00 414 414 ӂ
418
- 00415 00 415 415 Ӄ
419
- 00416 00 416 416 ӄ
420
- 00417 00 417 417 Ӆ
421
- 00418 00 418 418 ӆ
422
- 00419 00 419 419 Ӈ
423
- 00420 00 420 420 ӈ
424
- 00421 00 421 421 Ӊ
425
- 00422 00 422 422 ӊ
426
- 00423 00 423 423 Ӌ
427
- 00424 00 424 424 ӌ
428
- 00425 00 425 425 Ӎ
429
- 00426 00 426 426 ӎ
430
- 00427 00 427 427 ӏ
431
- 00428 00 428 428 Ӑ
432
- 00429 00 429 429 ӑ
433
- 00430 00 430 430 Ӓ
434
- 00431 00 431 431 ӓ
435
- 00432 00 432 432 Ӕ
436
- 00433 00 433 433 ӕ
437
- 00434 00 434 434 Ӗ
438
- 00435 00 435 435 ӗ
439
- 00436 00 436 436 Ә
440
- 00437 00 437 437 ә
441
- 00438 00 438 438 Ӛ
442
- 00439 00 439 439 ӛ
443
- 00440 00 440 440 Ӝ
444
- 00441 00 441 441 ӝ
445
- 00442 00 442 442 Ӟ
446
- 00443 00 443 443 ӟ
447
- 00444 00 444 444 Ӡ
448
- 00445 00 445 445 ӡ
449
- 00446 00 446 446 Ӣ
450
- 00447 00 447 447 ӣ
451
- 00448 00 448 448 Ӥ
452
- 00449 00 449 449 ӥ
453
- 00450 00 450 450 Ӧ
454
- 00451 00 451 451 ӧ
455
- 00452 00 452 452 Ө
456
- 00453 00 453 453 ө
457
- 00454 00 454 454 Ӫ
458
- 00455 00 455 455 ӫ
459
- 00456 00 456 456 Ӭ
460
- 00457 00 457 457 ӭ
461
- 00458 00 458 458 Ӯ
462
- 00459 00 459 459 ӯ
463
- 00460 00 460 460 Ӱ
464
- 00461 00 461 461 ӱ
465
- 00462 00 462 462 Ӳ
466
- 00463 00 463 463 ӳ
467
- 00464 00 464 464 Ӵ
468
- 00465 00 465 465 ӵ
469
- 00466 00 466 466 Ӷ
470
- 00467 00 467 467 ӷ
471
- 00468 00 468 468 Ӹ
472
- 00469 00 469 469 ӹ
473
- 00470 00 470 470 Ӻ
474
- 00471 00 471 471 ӻ
475
- 00472 00 472 472 Ӽ
476
- 00473 00 473 473 ӽ
477
- 00474 00 474 474 Ӿ
478
- 00475 00 475 475 ӿ
479
- 00476 00 476 476 Ԁ
480
- 00477 00 477 477 ԁ
481
- 00478 00 478 478 Ԃ
482
- 00479 00 479 479 ԃ
483
- 00480 00 480 480 Ԅ
484
- 00481 00 481 481 ԅ
485
- 00482 00 482 482 Ԇ
486
- 00483 00 483 483 ԇ
487
- 00484 00 484 484 Ԉ
488
- 00485 00 485 485 ԉ
489
- 00486 00 486 486 Ԋ
490
- 00487 00 487 487 ԋ
491
- 00488 00 488 488 Ԍ
492
- 00489 00 489 489 ԍ
493
- 00490 00 490 490 Ԏ
494
- 00491 00 491 491 ԏ
495
- 00492 00 492 492 Ԑ
496
- 00493 00 493 493 ԑ
497
- 00494 00 494 494 Ԓ
498
- 00495 00 495 495 ԓ
499
- 00496 00 496 496 Ԕ
500
- 00497 00 497 497 ԕ
501
- 00498 00 498 498 Ԗ
502
- 00499 00 499 499 ԗ
503
- 00500 00 500 500 Ԙ
504
- 00501 00 501 501 ԙ
505
- 00502 00 502 502 Ԛ
506
- 00503 00 503 503 ԛ
507
- 00504 00 504 504 Ԝ
508
- 00505 00 505 505 ԝ
509
- 00506 00 506 506 Ԟ
510
- 00507 00 507 507 ԟ
511
- 00508 00 508 508 Ԡ
512
- 00509 00 509 509 ԡ
513
- 00510 00 510 510 Ԣ
514
- 00511 00 511 511 ԣ
515
- 00512 00 512 512 Ԥ
516
- 00513 00 513 513 ԥ
517
- 00514 00 514 514 Ԧ
518
- 00515 00 515 515 ԧ
519
- 00516 00 516 516 Ԩ
520
- 00517 00 517 517 ԩ
521
- 00518 00 518 518 Ԫ
522
- 00519 00 519 519 ԫ
523
- 00520 00 520 520 Ԭ
524
- 00521 00 521 521 ԭ
525
- 00522 00 522 522 Ԯ
526
- 00523 00 523 523 ԯ
527
- 00524 00 524 524 ἀ
528
- 00525 00 525 525 ἁ
529
- 00526 00 526 526 ἂ
530
- 00527 00 527 527 ἃ
531
- 00528 00 528 528 ἄ
532
- 00529 00 529 529 ἅ
533
- 00530 00 530 530 ἆ
534
- 00531 00 531 531 ἇ
535
- 00532 00 532 532 Ἀ
536
- 00533 00 533 533 Ἁ
537
- 00534 00 534 534 Ἂ
538
- 00535 00 535 535 Ἃ
539
- 00536 00 536 536 Ἄ
540
- 00537 00 537 537 Ἅ
541
- 00538 00 538 538 Ἆ
542
- 00539 00 539 539 Ἇ
543
- 00540 00 540 540 ἐ
544
- 00541 00 541 541 ἑ
545
- 00542 00 542 542 ἒ
546
- 00543 00 543 543 ἓ
547
- 00544 00 544 544 ἔ
548
- 00545 00 545 545 ἕ
549
- 00546 00 546 546 Ἐ
550
- 00547 00 547 547 Ἑ
551
- 00548 00 548 548 Ἒ
552
- 00549 00 549 549 Ἓ
553
- 00550 00 550 550 Ἔ
554
- 00551 00 551 551 Ἕ
555
- 00552 00 552 552 ἠ
556
- 00553 00 553 553 ἡ
557
- 00554 00 554 554 ἢ
558
- 00555 00 555 555 ἣ
559
- 00556 00 556 556 ἤ
560
- 00557 00 557 557 ἥ
561
- 00558 00 558 558 ἦ
562
- 00559 00 559 559 ἧ
563
- 00560 00 560 560 Ἠ
564
- 00561 00 561 561 Ἡ
565
- 00562 00 562 562 Ἢ
566
- 00563 00 563 563 Ἣ
567
- 00564 00 564 564 Ἤ
568
- 00565 00 565 565 Ἥ
569
- 00566 00 566 566 Ἦ
570
- 00567 00 567 567 Ἧ
571
- 00568 00 568 568 ἰ
572
- 00569 00 569 569 ἱ
573
- 00570 00 570 570 ἲ
574
- 00571 00 571 571 ἳ
575
- 00572 00 572 572 ἴ
576
- 00573 00 573 573 ἵ
577
- 00574 00 574 574 ἶ
578
- 00575 00 575 575 ἷ
579
- 00576 00 576 576 Ἰ
580
- 00577 00 577 577 Ἱ
581
- 00578 00 578 578 Ἲ
582
- 00579 00 579 579 Ἳ
583
- 00580 00 580 580 Ἴ
584
- 00581 00 581 581 Ἵ
585
- 00582 00 582 582 Ἶ
586
- 00583 00 583 583 Ἷ
587
- 00584 00 584 584 ὀ
588
- 00585 00 585 585 ὁ
589
- 00586 00 586 586 ὂ
590
- 00587 00 587 587 ὃ
591
- 00588 00 588 588 ὄ
592
- 00589 00 589 589 ὅ
593
- 00590 00 590 590 Ὀ
594
- 00591 00 591 591 Ὁ
595
- 00592 00 592 592 Ὂ
596
- 00593 00 593 593 Ὃ
597
- 00594 00 594 594 Ὄ
598
- 00595 00 595 595 Ὅ
599
- 00596 00 596 596 ὐ
600
- 00597 00 597 597 ὑ
601
- 00598 00 598 598 ὒ
602
- 00599 00 599 599 ὓ
603
- 00600 00 600 600 ὔ
604
- 00601 00 601 601 ὕ
605
- 00602 00 602 602 ὖ
606
- 00603 00 603 603 ὗ
607
- 00604 00 604 604 Ὑ
608
- 00605 00 605 605 Ὓ
609
- 00606 00 606 606 Ὕ
610
- 00607 00 607 607 Ὗ
611
- 00608 00 608 608 ὠ
612
- 00609 00 609 609 ὡ
613
- 00610 00 610 610 ὢ
614
- 00611 00 611 611 ὣ
615
- 00612 00 612 612 ὤ
616
- 00613 00 613 613 ὥ
617
- 00614 00 614 614 ὦ
618
- 00615 00 615 615 ὧ
619
- 00616 00 616 616 Ὠ
620
- 00617 00 617 617 Ὡ
621
- 00618 00 618 618 Ὢ
622
- 00619 00 619 619 Ὣ
623
- 00620 00 620 620 Ὤ
624
- 00621 00 621 621 Ὥ
625
- 00622 00 622 622 Ὦ
626
- 00623 00 623 623 Ὧ
627
- 00624 00 624 624 ὰ
628
- 00625 00 625 625 ά
629
- 00626 00 626 626 ὲ
630
- 00627 00 627 627 έ
631
- 00628 00 628 628 ὴ
632
- 00629 00 629 629 ή
633
- 00630 00 630 630 ὶ
634
- 00631 00 631 631 ί
635
- 00632 00 632 632 ὸ
636
- 00633 00 633 633 ό
637
- 00634 00 634 634 ὺ
638
- 00635 00 635 635 ύ
639
- 00636 00 636 636 ὼ
640
- 00637 00 637 637 ώ
641
- 00638 00 638 638 ᾀ
642
- 00639 00 639 639 ᾁ
643
- 00640 00 640 640 ᾂ
644
- 00641 00 641 641 ᾃ
645
- 00642 00 642 642 ᾄ
646
- 00643 00 643 643 ᾅ
647
- 00644 00 644 644 ᾆ
648
- 00645 00 645 645 ᾇ
649
- 00646 00 646 646 ᾈ
650
- 00647 00 647 647 ᾉ
651
- 00648 00 648 648 ᾊ
652
- 00649 00 649 649 ᾋ
653
- 00650 00 650 650 ᾌ
654
- 00651 00 651 651 ᾍ
655
- 00652 00 652 652 ᾎ
656
- 00653 00 653 653 ᾏ
657
- 00654 00 654 654 ᾐ
658
- 00655 00 655 655 ᾑ
659
- 00656 00 656 656 ᾒ
660
- 00657 00 657 657 ᾓ
661
- 00658 00 658 658 ᾔ
662
- 00659 00 659 659 ᾕ
663
- 00660 00 660 660 ᾖ
664
- 00661 00 661 661 ᾗ
665
- 00662 00 662 662 ᾘ
666
- 00663 00 663 663 ᾙ
667
- 00664 00 664 664 ᾚ
668
- 00665 00 665 665 ᾛ
669
- 00666 00 666 666 ᾜ
670
- 00667 00 667 667 ᾝ
671
- 00668 00 668 668 ᾞ
672
- 00669 00 669 669 ᾟ
673
- 00670 00 670 670 ᾠ
674
- 00671 00 671 671 ᾡ
675
- 00672 00 672 672 ᾢ
676
- 00673 00 673 673 ᾣ
677
- 00674 00 674 674 ᾤ
678
- 00675 00 675 675 ᾥ
679
- 00676 00 676 676 ᾦ
680
- 00677 00 677 677 ᾧ
681
- 00678 00 678 678 ᾨ
682
- 00679 00 679 679 ᾩ
683
- 00680 00 680 680 ᾪ
684
- 00681 00 681 681 ᾫ
685
- 00682 00 682 682 ᾬ
686
- 00683 00 683 683 ᾭ
687
- 00684 00 684 684 ᾮ
688
- 00685 00 685 685 ᾯ
689
- 00686 00 686 686 ᾰ
690
- 00687 00 687 687 ᾱ
691
- 00688 00 688 688 ᾲ
692
- 00689 00 689 689 ᾳ
693
- 00690 00 690 690 ᾴ
694
- 00691 00 691 691 ᾶ
695
- 00692 00 692 692 ᾷ
696
- 00693 00 693 693 Ᾰ
697
- 00694 00 694 694 Ᾱ
698
- 00695 00 695 695 Ὰ
699
- 00696 00 696 696 Ά
700
- 00697 00 697 697 ᾼ
701
- 00698 00 698 698 ι
702
- 00699 00 699 699 ῂ
703
- 00700 00 700 700 ῃ
704
- 00701 00 701 701 ῄ
705
- 00702 00 702 702 ῆ
706
- 00703 00 703 703 ῇ
707
- 00704 00 704 704 Ὲ
708
- 00705 00 705 705 Έ
709
- 00706 00 706 706 Ὴ
710
- 00707 00 707 707 Ή
711
- 00708 00 708 708 ῌ
712
- 00709 00 709 709 ῐ
713
- 00710 00 710 710 ῑ
714
- 00711 00 711 711 ῒ
715
- 00712 00 712 712 ΐ
716
- 00713 00 713 713 ῖ
717
- 00714 00 714 714 ῗ
718
- 00715 00 715 715 Ῐ
719
- 00716 00 716 716 Ῑ
720
- 00717 00 717 717 Ὶ
721
- 00718 00 718 718 Ί
722
- 00719 00 719 719 ῠ
723
- 00720 00 720 720 ῡ
724
- 00721 00 721 721 ῢ
725
- 00722 00 722 722 ΰ
726
- 00723 00 723 723 ῤ
727
- 00724 00 724 724 ῥ
728
- 00725 00 725 725 ῦ
729
- 00726 00 726 726 ῧ
730
- 00727 00 727 727 Ῠ
731
- 00728 00 728 728 Ῡ
732
- 00729 00 729 729 Ὺ
733
- 00730 00 730 730 Ύ
734
- 00731 00 731 731 Ῥ
735
- 00732 00 732 732 ῲ
736
- 00733 00 733 733 ῳ
737
- 00734 00 734 734 ῴ
738
- 00735 00 735 735 ῶ
739
- 00736 00 736 736 ῷ
740
- 00737 00 737 737 Ὸ
741
- 00738 00 738 738 Ό
742
- 00739 00 739 739 Ὼ
743
- 00740 00 740 740 Ώ
744
- 00741 00 741 741 ῼ
745
- 00742 00 742 742 Ⲁ
746
- 00743 00 743 743 ⲁ
747
- 00744 00 744 744 Ⲃ
748
- 00745 00 745 745 ⲃ
749
- 00746 00 746 746 Ⲅ
750
- 00747 00 747 747 ⲅ
751
- 00748 00 748 748 Ⲇ
752
- 00749 00 749 749 ⲇ
753
- 00750 00 750 750 Ⲉ
754
- 00751 00 751 751 ⲉ
755
- 00752 00 752 752 Ⲋ
756
- 00753 00 753 753 ⲋ
757
- 00754 00 754 754 Ⲍ
758
- 00755 00 755 755 ⲍ
759
- 00756 00 756 756 Ⲏ
760
- 00757 00 757 757 ⲏ
761
- 00758 00 758 758 Ⲑ
762
- 00759 00 759 759 ⲑ
763
- 00760 00 760 760 Ⲓ
764
- 00761 00 761 761 ⲓ
765
- 00762 00 762 762 Ⲕ
766
- 00763 00 763 763 ⲕ
767
- 00764 00 764 764 Ⲗ
768
- 00765 00 765 765 ⲗ
769
- 00766 00 766 766 Ⲙ
770
- 00767 00 767 767 ⲙ
771
- 00768 00 768 768 Ⲛ
772
- 00769 00 769 769 ⲛ
773
- 00770 00 770 770 Ⲝ
774
- 00771 00 771 771 ⲝ
775
- 00772 00 772 772 Ⲟ
776
- 00773 00 773 773 ⲟ
777
- 00774 00 774 774 Ⲡ
778
- 00775 00 775 775 ⲡ
779
- 00776 00 776 776 Ⲣ
780
- 00777 00 777 777 ⲣ
781
- 00778 00 778 778 Ⲥ
782
- 00779 00 779 779 ⲥ
783
- 00780 00 780 780 Ⲧ
784
- 00781 00 781 781 ⲧ
785
- 00782 00 782 782 Ⲩ
786
- 00783 00 783 783 ⲩ
787
- 00784 00 784 784 Ⲫ
788
- 00785 00 785 785 ⲫ
789
- 00786 00 786 786 Ⲭ
790
- 00787 00 787 787 ⲭ
791
- 00788 00 788 788 Ⲯ
792
- 00789 00 789 789 ⲯ
793
- 00790 00 790 790 Ⲱ
794
- 00791 00 791 791 ⲱ
795
- 00792 00 792 792 Ⲳ
796
- 00793 00 793 793 ⲳ
797
- 00794 00 794 794 Ⲵ
798
- 00795 00 795 795 ⲵ
799
- 00796 00 796 796 Ⲷ
800
- 00797 00 797 797 ⲷ
801
- 00798 00 798 798 Ⲹ
802
- 00799 00 799 799 ⲹ
803
- 00800 00 800 800 Ⲻ
804
- 00801 00 801 801 ⲻ
805
- 00802 00 802 802 Ⲽ
806
- 00803 00 803 803 ⲽ
807
- 00804 00 804 804 Ⲿ
808
- 00805 00 805 805 ⲿ
809
- 00806 00 806 806 Ⳁ
810
- 00807 00 807 807 ⳁ
811
- 00808 00 808 808 Ⳃ
812
- 00809 00 809 809 ⳃ
813
- 00810 00 810 810 Ⳅ
814
- 00811 00 811 811 ⳅ
815
- 00812 00 812 812 Ⳇ
816
- 00813 00 813 813 ⳇ
817
- 00814 00 814 814 Ⳉ
818
- 00815 00 815 815 ⳉ
819
- 00816 00 816 816 Ⳋ
820
- 00817 00 817 817 ⳋ
821
- 00818 00 818 818 Ⳍ
822
- 00819 00 819 819 ⳍ
823
- 00820 00 820 820 Ⳏ
824
- 00821 00 821 821 ⳏ
825
- 00822 00 822 822 Ⳑ
826
- 00823 00 823 823 ⳑ
827
- 00824 00 824 824 Ⳓ
828
- 00825 00 825 825 ⳓ
829
- 00826 00 826 826 Ⳕ
830
- 00827 00 827 827 ⳕ
831
- 00828 00 828 828 Ⳗ
832
- 00829 00 829 829 ⳗ
833
- 00830 00 830 830 Ⳙ
834
- 00831 00 831 831 ⳙ
835
- 00832 00 832 832 Ⳛ
836
- 00833 00 833 833 ⳛ
837
- 00834 00 834 834 Ⳝ
838
- 00835 00 835 835 ⳝ
839
- 00836 00 836 836 Ⳟ
840
- 00837 00 837 837 ⳟ
841
- 00838 00 838 838 Ⳡ
842
- 00839 00 839 839 ⳡ
843
- 00840 00 840 840 Ⳣ
844
- 00841 00 841 841 ⳣ
845
- 00842 00 842 842 ⳤ
846
- 00843 00 843 843 Ⳬ
847
- 00844 00 844 844 ⳬ
848
- 00845 00 845 845 Ⳮ
849
- 00846 00 846 846 ⳮ
850
- 00847 00 847 847 Ⳳ
851
- 00848 00 848 848 ⳳ
852
- 00849 00 849 849 Ꙁ
853
- 00850 00 850 850 ꙁ
854
- 00851 00 851 851 Ꙃ
855
- 00852 00 852 852 ꙃ
856
- 00853 00 853 853 Ꙅ
857
- 00854 00 854 854 ꙅ
858
- 00855 00 855 855 Ꙇ
859
- 00856 00 856 856 ꙇ
860
- 00857 00 857 857 Ꙉ
861
- 00858 00 858 858 ꙉ
862
- 00859 00 859 859 Ꙋ
863
- 00860 00 860 860 ꙋ
864
- 00861 00 861 861 Ꙍ
865
- 00862 00 862 862 ꙍ
866
- 00863 00 863 863 Ꙏ
867
- 00864 00 864 864 ꙏ
868
- 00865 00 865 865 Ꙑ
869
- 00866 00 866 866 ꙑ
870
- 00867 00 867 867 Ꙓ
871
- 00868 00 868 868 ꙓ
872
- 00869 00 869 869 Ꙕ
873
- 00870 00 870 870 ꙕ
874
- 00871 00 871 871 Ꙗ
875
- 00872 00 872 872 ꙗ
876
- 00873 00 873 873 Ꙙ
877
- 00874 00 874 874 ꙙ
878
- 00875 00 875 875 Ꙛ
879
- 00876 00 876 876 ꙛ
880
- 00877 00 877 877 Ꙝ
881
- 00878 00 878 878 ꙝ
882
- 00879 00 879 879 Ꙟ
883
- 00880 00 880 880 ꙟ
884
- 00881 00 881 881 Ꙡ
885
- 00882 00 882 882 ꙡ
886
- 00883 00 883 883 Ꙣ
887
- 00884 00 884 884 ꙣ
888
- 00885 00 885 885 Ꙥ
889
- 00886 00 886 886 ꙥ
890
- 00887 00 887 887 Ꙧ
891
- 00888 00 888 888 ꙧ
892
- 00889 00 889 889 Ꙩ
893
- 00890 00 890 890 ꙩ
894
- 00891 00 891 891 Ꙫ
895
- 00892 00 892 892 ꙫ
896
- 00893 00 893 893 Ꙭ
897
- 00894 00 894 894 ꙭ
898
- 00895 00 895 895 ꙮ
899
- 00896 00 896 896 ꙿ
900
- 00897 00 897 897 Ꚁ
901
- 00898 00 898 898 ꚁ
902
- 00899 00 899 899 Ꚃ
903
- 00900 00 900 900 ꚃ
904
- 00901 00 901 901 Ꚅ
905
- 00902 00 902 902 ꚅ
906
- 00903 00 903 903 Ꚇ
907
- 00904 00 904 904 ꚇ
908
- 00905 00 905 905 Ꚉ
909
- 00906 00 906 906 ꚉ
910
- 00907 00 907 907 Ꚋ
911
- 00908 00 908 908 ꚋ
912
- 00909 00 909 909 Ꚍ
913
- 00910 00 910 910 ꚍ
914
- 00911 00 911 911 Ꚏ
915
- 00912 00 912 912 ꚏ
916
- 00913 00 913 913 Ꚑ
917
- 00914 00 914 914 ꚑ
918
- 00915 00 915 915 Ꚓ
919
- 00916 00 916 916 ꚓ
920
- 00917 00 917 917 Ꚕ
921
- 00918 00 918 918 ꚕ
922
- 00919 00 919 919 Ꚗ
923
- 00920 00 920 920 ꚗ
924
- 00921 00 921 921 Ꚙ
925
- 00922 00 922 922 ꚙ
926
- 00923 00 923 923 Ꚛ
927
- 00924 00 924 924 ꚛ
928
- 00925 00 925 925 ꚜ
929
- 00926 00 926 926 ꚝ
930
- 00927 00 927 927 ᲀ
931
- 00928 00 928 928 ᲁ
932
- 00929 00 929 929 ᲂ
933
- 00930 00 930 930 ᲃ
934
- 00931 00 931 931 ᲄ
935
- 00932 00 932 932 ᲅ
936
- 00933 00 933 933 ᲆ
937
- 00934 00 934 934 ᲇ
938
- 00935 00 935 935 ᲈ
939
- 00936 00 936 936 Ա
940
- 00937 00 937 937 Բ
941
- 00938 00 938 938 Գ
942
- 00939 00 939 939 Դ
943
- 00940 00 940 940 Ե
944
- 00941 00 941 941 Զ
945
- 00942 00 942 942 Է
946
- 00943 00 943 943 Ը
947
- 00944 00 944 944 Թ
948
- 00945 00 945 945 Ժ
949
- 00946 00 946 946 Ի
950
- 00947 00 947 947 Լ
951
- 00948 00 948 948 Խ
952
- 00949 00 949 949 Ծ
953
- 00950 00 950 950 Կ
954
- 00951 00 951 951 Հ
955
- 00952 00 952 952 Ձ
956
- 00953 00 953 953 Ղ
957
- 00954 00 954 954 Ճ
958
- 00955 00 955 955 Մ
959
- 00956 00 956 956 Յ
960
- 00957 00 957 957 Ն
961
- 00958 00 958 958 Շ
962
- 00959 00 959 959 Ո
963
- 00960 00 960 960 Չ
964
- 00961 00 961 961 Պ
965
- 00962 00 962 962 Ջ
966
- 00963 00 963 963 Ռ
967
- 00964 00 964 964 Ս
968
- 00965 00 965 965 Վ
969
- 00966 00 966 966 Տ
970
- 00967 00 967 967 Ր
971
- 00968 00 968 968 Ց
972
- 00969 00 969 969 Ւ
973
- 00970 00 970 970 Փ
974
- 00971 00 971 971 Ք
975
- 00972 00 972 972 Օ
976
- 00973 00 973 973 Ֆ
977
- 00974 00 974 974 ՙ
978
- 00975 00 975 975 ՠ
979
- 00976 00 976 976 ա
980
- 00977 00 977 977 բ
981
- 00978 00 978 978 գ
982
- 00979 00 979 979 դ
983
- 00980 00 980 980 ե
984
- 00981 00 981 981 զ
985
- 00982 00 982 982 է
986
- 00983 00 983 983 ը
987
- 00984 00 984 984 թ
988
- 00985 00 985 985 ժ
989
- 00986 00 986 986 ի
990
- 00987 00 987 987 լ
991
- 00988 00 988 988 խ
992
- 00989 00 989 989 ծ
993
- 00990 00 990 990 կ
994
- 00991 00 991 991 հ
995
- 00992 00 992 992 ձ
996
- 00993 00 993 993 ղ
997
- 00994 00 994 994 ճ
998
- 00995 00 995 995 մ
999
- 00996 00 996 996 յ
1000
- 00997 00 997 997 ն
1001
- 00998 00 998 998 շ
1002
- 00999 00 999 999 ո
1003
- 01000 01 000 1000 A
1004
- 01001 01 001 1001 B
1005
- 01002 01 002 1002 C
1006
- 01003 01 003 1003 D
1007
- 01004 01 004 1004 E
1008
- 01005 01 005 1005 F
1009
- 01006 01 006 1006 G
1010
- 01007 01 007 1007 H
1011
- 01008 01 008 1008 I
1012
- 01009 01 009 1009 J
1013
- 01010 01 010 1010 K
1014
- 01011 01 011 1011 L
1015
- 01012 01 012 1012 M
1016
- 01013 01 013 1013 N
1017
- 01014 01 014 1014 O
1018
- 01015 01 015 1015 P
1019
- 01016 01 016 1016 Q
1020
- 01017 01 017 1017 R
1021
- 01018 01 018 1018 S
1022
- 01019 01 019 1019 T
1023
- 01020 01 020 1020 U
1024
- 01021 01 021 1021 V
1025
- 01022 01 022 1022 W
1026
- 01023 01 023 1023 X
1027
- 01024 01 024 1024 Y
1028
- 01025 01 025 1025 Z
1029
- 01026 01 026 1026 a
1030
- 01027 01 027 1027 b
1031
- 01028 01 028 1028 c
1032
- 01029 01 029 1029 d
1033
- 01030 01 030 1030 e
1034
- 01031 01 031 1031 f
1035
- 01032 01 032 1032 g
1036
- 01033 01 033 1033 h
1037
- 01034 01 034 1034 i
1038
- 01035 01 035 1035 j
1039
- 01036 01 036 1036 k
1040
- 01037 01 037 1037 l
1041
- 01038 01 038 1038 m
1042
- 01039 01 039 1039 n
1043
- 01040 01 040 1040 o
1044
- 01041 01 041 1041 p
1045
- 01042 01 042 1042 q
1046
- 01043 01 043 1043 r
1047
- 01044 01 044 1044 s
1048
- 01045 01 045 1045 t
1049
- 01046 01 046 1046 u
1050
- 01047 01 047 1047 v
1051
- 01048 01 048 1048 w
1052
- 01049 01 049 1049 x
1053
- 01050 01 050 1050 y
1054
- 01051 01 051 1051 z
1055
- 01052 01 052 1052 À
1056
- 01053 01 053 1053 È
1057
- 01054 01 054 1054 É
1058
- 01055 01 055 1055 Ì
1059
- 01056 01 056 1056 Ò
1060
- 01057 01 057 1057 Ù
1061
- 01058 01 058 1058 à
1062
- 01059 01 059 1059 è
1063
- 01060 01 060 1060 é
1064
- 01061 01 061 1061 ì
1065
- 01062 01 062 1062 ò
1066
- 01063 01 063 1063 ù
1067
- 01064 01 064 1064 Ä
1068
- 01065 01 065 1065 ä
1069
- 01066 01 066 1066 Ö
1070
- 01067 01 067 1067 ö
1071
- 01068 01 068 1068 Ü
1072
- 01069 01 069 1069 ü
1073
- 01070 01 070 1070 ß
1074
- 01071 01 071 1071 Ç
1075
- 01072 01 072 1072 ç
1076
- 01073 01 073 1073 Ë
1077
- 01074 01 074 1074 ë
1078
- 01075 01 075 1075 Á
1079
- 01076 01 076 1076 á
1080
- 01077 01 077 1077 Ñ
1081
- 01078 01 078 1078 ñ
1082
- 01079 01 079 1079 Â
1083
- 01080 01 080 1080 il
1084
- 01081 01 081 1081 lo
1085
- 01082 01 082 1082 la
1086
- 01083 01 083 1083 gli
1087
- 01084 01 084 1084 le
1088
- 01085 01 085 1085 un
1089
- 01086 01 086 1086 uno
1090
- 01087 01 087 1087 una
1091
- 01088 01 088 1088 l'
1092
- 01089 01 089 1089 un'
1093
- 01090 01 090 1090 ad
1094
- 01091 01 091 1091 di
1095
- 01092 01 092 1092 d'
1096
- 01093 01 093 1093 da
1097
- 01094 01 094 1094 in
1098
- 01095 01 095 1095 con
1099
- 01096 01 096 1096 su
1100
- 01097 01 097 1097 per
1101
- 01098 01 098 1098 tra
1102
- 01099 01 099 1099 fra
1103
- 01100 01 100 1100 del
1104
- 01101 01 101 1101 dello
1105
- 01102 01 102 1102 della
1106
- 01103 01 103 1103 dei
1107
- 01104 01 104 1104 degli
1108
- 01105 01 105 1105 delle
1109
- 01106 01 106 1106 dell'
1110
- 01107 01 107 1107 al
1111
- 01108 01 108 1108 allo
1112
- 01109 01 109 1109 alla
1113
- 01110 01 110 1110 ai
1114
- 01111 01 111 1111 agli
1115
- 01112 01 112 1112 alle
1116
- 01113 01 113 1113 all'
1117
- 01114 01 114 1114 dal
1118
- 01115 01 115 1115 dallo
1119
- 01116 01 116 1116 dalla
1120
- 01117 01 117 1117 dai
1121
- 01118 01 118 1118 dagli
1122
- 01119 01 119 1119 dalle
1123
- 01120 01 120 1120 dall'
1124
- 01121 01 121 1121 nel
1125
- 01122 01 122 1122 nello
1126
- 01123 01 123 1123 nella
1127
- 01124 01 124 1124 nei
1128
- 01125 01 125 1125 negli
1129
- 01126 01 126 1126 nelle
1130
- 01127 01 127 1127 nell'
1131
- 01128 01 128 1128 sul
1132
- 01129 01 129 1129 sullo
1133
- 01130 01 130 1130 sulla
1134
- 01131 01 131 1131 sui
1135
- 01132 01 132 1132 sugli
1136
- 01133 01 133 1133 sulle
1137
- 01134 01 134 1134 sull'
1138
- 01135 01 135 1135 ed
1139
- 01136 01 136 1136 oppure
1140
- 01137 01 137 1137 ma
1141
- 01138 01 138 1138 però
1142
- 01139 01 139 1139 bensì
1143
- 01140 01 140 1140 se
1144
- 01141 01 141 1141 che
1145
- 01142 01 142 1142 perché
1146
- 01143 01 143 1143 quando
1147
- 01144 01 144 1144 mentre
1148
- 01145 01 145 1145 quindi
1149
- 01146 01 146 1146 dunque
1150
- 01147 01 147 1147 ossia
1151
- 01148 01 148 1148 cioè
1152
- 01149 01 149 1149 né
1153
- 01150 01 150 1150 io
1154
- 01151 01 151 1151 tu
1155
- 01152 01 152 1152 lui
1156
- 01153 01 153 1153 lei
1157
- 01154 01 154 1154 noi
1158
- 01155 01 155 1155 voi
1159
- 01156 01 156 1156 loro
1160
- 01157 01 157 1157 me
1161
- 01158 01 158 1158 te
1162
- 01159 01 159 1159 sé
1163
- 01160 01 160 1160 mi
1164
- 01161 01 161 1161 ti
1165
- 01162 01 162 1162 ci
1166
- 01163 01 163 1163 vi
1167
- 01164 01 164 1164 si
1168
- 01165 01 165 1165 li
1169
- 01166 01 166 1166 ne
1170
- 01167 01 167 1167 ce
1171
- 01168 01 168 1168 ve
1172
- 01169 01 169 1169 m'
1173
- 01170 01 170 1170 t'
1174
- 01171 01 171 1171 c'
1175
- 01172 01 172 1172 v'
1176
- 01173 01 173 1173 s'
1177
- 01174 01 174 1174 n'
1178
- 01175 01 175 1175 gl'
1179
- 01176 01 176 1176 glielo
1180
- 01177 01 177 1177 gliela
1181
- 01178 01 178 1178 glieli
1182
- 01179 01 179 1179 gliele
1183
- 01180 01 180 1180 gliel'
1184
- 01181 01 181 1181 questo
1185
- 01182 01 182 1182 questa
1186
- 01183 01 183 1183 questi
1187
- 01184 01 184 1184 queste
1188
- 01185 01 185 1185 quest'
1189
- 01186 01 186 1186 quello
1190
- 01187 01 187 1187 quella
1191
- 01188 01 188 1188 quelli
1192
- 01189 01 189 1189 quelle
1193
- 01190 01 190 1190 quell'
1194
- 01191 01 191 1191 ciò
1195
- 01192 01 192 1192 chi
1196
- 01193 01 193 1193 cui
1197
- 01194 01 194 1194 qual
1198
- 01195 01 195 1195 quale
1199
- 01196 01 196 1196 quali
1200
- 01197 01 197 1197 non
1201
- 01198 01 198 1198 essere
1202
- 01199 01 199 1199 esser
1203
- 01200 01 200 1200 sono
1204
- 01201 01 201 1201 sei
1205
- 01202 01 202 1202 siamo
1206
- 01203 01 203 1203 siete
1207
- 01204 01 204 1204 era
1208
- 01205 01 205 1205 stato
1209
- 01206 01 206 1206 stata
1210
- 01207 01 207 1207 stati
1211
- 01208 01 208 1208 state
1212
- 01209 01 209 1209 avere
1213
- 01210 01 210 1210 aver
1214
- 01211 01 211 1211 ho
1215
- 01212 01 212 1212 hai
1216
- 01213 01 213 1213 ha
1217
- 01214 01 214 1214 abbiamo
1218
- 01215 01 215 1215 avete
1219
- 01216 01 216 1216 hanno
1220
- 01217 01 217 1217 avuto
1221
- 01218 01 218 1218 potere
1222
- 01219 01 219 1219 posso
1223
- 01220 01 220 1220 puoi
1224
- 01221 01 221 1221 può
1225
- 01222 01 222 1222 possiamo
1226
- 01223 01 223 1223 potete
1227
- 01224 01 224 1224 possono
1228
- 01225 01 225 1225 dovere
1229
- 01226 01 226 1226 devo
1230
- 01227 01 227 1227 devi
1231
- 01228 01 228 1228 deve
1232
- 01229 01 229 1229 dobbiamo
1233
- 01230 01 230 1230 dovete
1234
- 01231 01 231 1231 devono
1235
- 01232 01 232 1232 volere
1236
- 01233 01 233 1233 voglio
1237
- 01234 01 234 1234 vuoi
1238
- 01235 01 235 1235 vuole
1239
- 01236 01 236 1236 vogliamo
1240
- 01237 01 237 1237 volete
1241
- 01238 01 238 1238 vogliono
1242
- 01239 01 239 1239 più
1243
- 01240 01 240 1240 meno
1244
- 01241 01 241 1241 molto
1245
- 01242 01 242 1242 poco
1246
- 01243 01 243 1243 già
1247
- 01244 01 244 1244 ancora
1248
- 01245 01 245 1245 sempre
1249
- 01246 01 246 1246 mai
1250
- 01247 01 247 1247 anche
1251
- 01248 01 248 1248 solo
1252
- 01249 01 249 1249 come
1253
- 01250 01 250 1250 dove
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vocab_stages/01_parole_by_id.meta.json DELETED
@@ -1,100 +0,0 @@
1
- {
2
- "name": "parole",
3
- "size": 17908,
4
- "path": "/Users/thomasseri/Desktop/columns/geodesic_mesh_nn/vocab_stages/01_parole_by_id.txt",
5
- "meta": {
6
- "min_word_freq": 3,
7
- "n_candidates": 17902,
8
- "source": "reconstructed_from_v1_pezzi",
9
- "top": [
10
- [
11
- "ingredient",
12
- 32409
13
- ],
14
- [
15
- "id",
16
- 22506
17
- ],
18
- [
19
- "the",
20
- 17636
21
- ],
22
- [
23
- "in",
24
- 13718
25
- ],
26
- [
27
- "of",
28
- 12175
29
- ],
30
- [
31
- "risposta",
32
- 11496
33
- ],
34
- [
35
- "and",
36
- 11119
37
- ],
38
- [
39
- "di",
40
- 10760
41
- ],
42
- [
43
- "is",
44
- 10074
45
- ],
46
- [
47
- "il",
48
- 7057
49
- ],
50
- [
51
- "to",
52
- 6645
53
- ],
54
- [
55
- "that",
56
- 6058
57
- ],
58
- [
59
- "la",
60
- 6018
61
- ],
62
- [
63
- "on",
64
- 5946
65
- ],
66
- [
67
- "or",
68
- 5603
69
- ],
70
- [
71
- "preparation",
72
- 5436
73
- ],
74
- [
75
- "context",
76
- 4037
77
- ],
78
- [
79
- "have",
80
- 3965
81
- ],
82
- [
83
- "as",
84
- 3952
85
- ],
86
- [
87
- "con",
88
- 3823
89
- ]
90
- ]
91
- },
92
- "specials": [
93
- "<|end|>",
94
- "<|bos|>",
95
- "<|eos|>",
96
- "<|pad|>",
97
- "<|unk|>",
98
- "<|sep|>"
99
- ]
100
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vocab_stages/01_parole_by_id.txt DELETED
The diff for this file is too large to render. See raw diff
 
vocab_stages/02_unioni_by_id.meta.json DELETED
@@ -1,122 +0,0 @@
1
- {
2
- "name": "unioni",
3
- "size": 20006,
4
- "path": "/Users/thomasseri/Desktop/columns/geodesic_mesh_nn/vocab_stages/02_unioni_by_id.txt",
5
- "meta": {
6
- "min_union_freq": 5,
7
- "n_candidates": 20000,
8
- "source": "frequent_unions_on_v2_surfaces",
9
- "n_trigram": 6295,
10
- "n_bigram": 13705,
11
- "top": [
12
- [
13
- "id w",
14
- 20700,
15
- "bi"
16
- ],
17
- [
18
- "ingredient id w",
19
- 16200,
20
- "tri"
21
- ],
22
- [
23
- "ingredient ingredient id",
24
- 16200,
25
- "tri"
26
- ],
27
- [
28
- "ingredient id",
29
- 16200,
30
- "bi"
31
- ],
32
- [
33
- "ingredient ingredient",
34
- 16200,
35
- "bi"
36
- ],
37
- [
38
- "id w ingredient",
39
- 15300,
40
- "tri"
41
- ],
42
- [
43
- "w ingredient ingredient",
44
- 15300,
45
- "tri"
46
- ],
47
- [
48
- "w ingredient",
49
- 15300,
50
- "bi"
51
- ],
52
- [
53
- "1 1",
54
- 9522,
55
- "bi"
56
- ],
57
- [
58
- "0 0",
59
- 8845,
60
- "bi"
61
- ],
62
- [
63
- "1 1 1",
64
- 8760,
65
- "tri"
66
- ],
67
- [
68
- "0 0 0",
69
- 6501,
70
- "tri"
71
- ],
72
- [
73
- "id w preparation",
74
- 4500,
75
- "tri"
76
- ],
77
- [
78
- "preparation id w",
79
- 4500,
80
- "tri"
81
- ],
82
- [
83
- "w preparation id",
84
- 4500,
85
- "tri"
86
- ],
87
- [
88
- "preparation id",
89
- 4500,
90
- "bi"
91
- ],
92
- [
93
- "w preparation",
94
- 4500,
95
- "bi"
96
- ],
97
- [
98
- "i don't",
99
- 3431,
100
- "bi"
101
- ],
102
- [
103
- "on that",
104
- 3431,
105
- "bi"
106
- ],
107
- [
108
- "a i",
109
- 3427,
110
- "bi"
111
- ]
112
- ]
113
- },
114
- "specials": [
115
- "<|end|>",
116
- "<|bos|>",
117
- "<|eos|>",
118
- "<|pad|>",
119
- "<|unk|>",
120
- "<|sep|>"
121
- ]
122
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vocab_stages/02_unioni_by_id.txt DELETED
The diff for this file is too large to render. See raw diff
 
vocab_stages/counts_summary.json DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "pipeline": "v1_pezzi → v2_parole(from pezzi) → v3_unioni(from v2)",
3
- "n_word_types": 31186,
4
- "n_bigram_types": 190363,
5
- "n_trigram_types": 304771,
6
- "parole_kept": 17902,
7
- "unioni_kept": 20000,
8
- "min_word_freq": 3,
9
- "min_union_freq": 5
10
- }
 
 
 
 
 
 
 
 
 
 
 
vocab_stages/manifest.json DELETED
@@ -1,25 +0,0 @@
1
- {
2
- "design": "multi_vocab_stages_cascade",
3
- "note": "Tre id-space separati in SERIE: testo→V1 pezzi (~1200)→V2 parole→V3 unioni. NON encode greedy parallelo. ~1200 = scala goTommy2 (1251), non 1.2M.",
4
- "stages": {
5
- "0_pezzi": {
6
- "path": "/Users/thomasseri/Desktop/columns/geodesic_mesh_nn/vocab_stages/00_pezzi_by_id.txt",
7
- "size": 1251
8
- },
9
- "1_parole": {
10
- "path": "/Users/thomasseri/Desktop/columns/geodesic_mesh_nn/vocab_stages/01_parole_by_id.txt",
11
- "size": 17908
12
- },
13
- "2_unioni": {
14
- "path": "/Users/thomasseri/Desktop/columns/geodesic_mesh_nn/vocab_stages/02_unioni_by_id.txt",
15
- "size": 20006
16
- }
17
- },
18
- "encode": "encode_cascade = encode_v1 → pass_to_v2 → pass_to_v3. Wiring train: 3 stadi in serie (emb/mesh per stadio), non stream paralleli.",
19
- "api": {
20
- "encode_v1": "text → pezzi_ids",
21
- "pass_to_v2": "pezzi_ids → parole_ids (aggrega pezzi)",
22
- "pass_to_v3": "parole surfaces → unioni (2/3 se frequenti)",
23
- "encode_cascade": "encode_v1 → pass_to_v2 → pass_to_v3"
24
- }
25
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
watchman_keystone_r08.py DELETED
@@ -1,416 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- ================================================================================
5
- SENTINEL R-08 (v7, v7.2, v7.2a) — FACCIA DI VEGLIA A CHIAVE DI VOLTA
6
- NON-LINEARE (GELU + SiLU + Tanh) + SIGILLO LICENZA SHA3-512
7
- ================================================================================
8
- Autore & Inventore: Thomas Seri (Sentinel - https://www.sentinelhub.eu)
9
- Foro Esclusivo: Tribunale di Milano (Sezione Specializzata in Materia di Impresa)
10
-
11
- Modulo di protezione architetturale ed etica integrata nei pesi neurali
12
- ai sensi della Licenza R08 v2.0 (Sezioni 3, 3.1 e 4).
13
- ================================================================================
14
- """
15
-
16
- import base64
17
- import binascii
18
- import hashlib
19
- import re
20
- import unicodedata
21
- from pathlib import Path
22
- from typing import Dict, Tuple
23
-
24
- import numpy as np
25
- import torch
26
- import torch.nn as nn
27
- import torch.nn.functional as F
28
-
29
-
30
- OFFICIAL_SENTINEL_LICENSE_SUMMARY = (
31
- "R08 — NON-COMMERCIAL RESEARCH & ETHICAL LICENSE (v2.0) | "
32
- "Copyright (c) 2026 Thomas Seri (Sentinel - https://www.sentinelhub.eu). All Rights Reserved. | "
33
- "Strictly Forbidden: Commercial Use, Reverse Engineering, Model Distillation, "
34
- "Military/Warfare/Weapons/UAVs, Pornography/NSFW/Inappropriate Images/Deepfakes/CSAM, "
35
- "Cyberattacks/Malware/PLC Sabotage, Gambling/Casinos/Illicit Drugs/Self-Harm, "
36
- "Political Propaganda/Electoral Manipulation, Religious Indoctrination, "
37
- "Mass Biometric Surveillance/GDPR PII Violation, Unsupervised High-Risk Medical/Financial."
38
- )
39
-
40
- _CONSTANTS_CACHE: Dict[Tuple[str, bytes, str, int], Tuple[torch.Tensor, torch.Tensor, torch.Tensor, str]] = {}
41
-
42
-
43
- def derive_license_nonlinear_constants(
44
- license_text: str = OFFICIAL_SENTINEL_LICENSE_SUMMARY,
45
- d_model: int = 256,
46
- scanner_bytecode: bytes = b"",
47
- owner_passphrase: str = ""
48
- ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, str]:
49
- """
50
- Deriva dall'impronta Post-Quantum SHA3-512 + SHAKE-256 della Licenza Ufficiale
51
- e del Bytecode dello Scanner Etico i tre operatori della varietà non-lineare:
52
- 1. q_lic : matrice ortogonale d_model x d_model
53
- 2. gate_lic: modulazione non-lineare di fase (1 x 1 x d_model//2)
54
- 3. bias_lic: portante armonica non-lineare (1 x 1 x d_model//2)
55
- """
56
- bc_hash = hashlib.sha256(scanner_bytecode).digest()
57
- cache_key = (license_text, bc_hash, owner_passphrase, d_model)
58
- cached = _CONSTANTS_CACHE.get(cache_key)
59
- if cached is not None:
60
- return cached
61
-
62
- payload = license_text.encode("utf-8") + b"::BYTECODE_SEAL::" + scanner_bytecode
63
- digest_512 = hashlib.sha3_512(payload).digest()
64
- if owner_passphrase:
65
- digest_512 = hashlib.pbkdf2_hmac(
66
- "sha512",
67
- owner_passphrase.encode("utf-8"),
68
- digest_512,
69
- iterations=600_000,
70
- dklen=64
71
- )
72
- fingerprint = hashlib.sha3_256(digest_512).hexdigest()[:16]
73
-
74
- half_d = d_model // 2
75
- total_floats = d_model * d_model + 2 * half_d
76
- stream = hashlib.shake_256(digest_512).digest(total_floats * 4)
77
- u32 = np.frombuffer(stream, dtype=np.uint32).astype(np.float64)
78
-
79
- u1 = np.clip((u32[0::2] + 0.5) / 4294967296.0, 1e-12, 1.0 - 1e-12)
80
- u2 = (u32[1::2] + 0.5) / 4294967296.0
81
- z0 = np.sqrt(-2.0 * np.log(u1)) * np.cos(2.0 * np.pi * u2)
82
- z1 = np.sqrt(-2.0 * np.log(u1)) * np.sin(2.0 * np.pi * u2)
83
- gauss = np.empty(total_floats, dtype=np.float32)
84
- gauss[0::2] = z0.astype(np.float32)
85
- gauss[1::2] = z1.astype(np.float32)
86
-
87
- mat_raw = torch.from_numpy(gauss[:d_model * d_model].reshape(d_model, d_model))
88
- gate_raw = torch.from_numpy(gauss[d_model * d_model:d_model * d_model + half_d].reshape(1, 1, half_d))
89
- bias_raw = torch.from_numpy(gauss[d_model * d_model + half_d:].reshape(1, 1, half_d))
90
-
91
- q_lic, _ = torch.linalg.qr(mat_raw)
92
- gate_lic = 1.25 + 1.15 * torch.sigmoid(gate_raw)
93
- bias_lic = bias_raw * 2.50
94
-
95
- res = (q_lic, gate_lic, bias_lic, fingerprint)
96
- if len(_CONSTANTS_CACHE) < 32:
97
- _CONSTANTS_CACHE[cache_key] = res
98
- return res
99
-
100
-
101
- class NonLinearWatchmanKeystoneR08(nn.Module):
102
- """
103
- Faccia di Veglia a Chiave di Volta Simplettica Non-Lineare per R-08 (v7, v7.2, v7.2a).
104
- """
105
- LEET_MAP = str.maketrans({
106
- "0": "o", "1": "i", "3": "e", "4": "a", "5": "s",
107
- "7": "t", "8": "b", "@": "a", "$": "s", "!": "i"
108
- })
109
-
110
- FORBIDDEN_ROOTS = (
111
- # 1. PORNOGRAFIA, IMMAGINI INAPPROPRIATE, NSFW, DEEPFAKE NON-CONSENSUALI, CSAM (Sez. 3.1)
112
- "pornograf", "porno", "immaginiinappropriat", "immagineinappropriat",
113
- "fotoinappropriat", "immagininud", "fotonud", "nudointegrale",
114
- "immaginihot", "contenutisessual", "sessualmenteesplicit", "immagininsfw",
115
- "generansu", "nsfwimage", "explicitsexual", "nudeimage", "nudephoto",
116
- "undressai", "deepfakenud", "deepfakesessual", "nonconsensualdeepfake",
117
- "revengeporn", "pedoporn", "csam", "childsexual", "abusesessual",
118
- "sfruttamentosessual", "eroticimage", "hentaiexplicit", "sexuallyexplicit",
119
- "imagesexuelle", "imagenessexuales", "pornographie", "nacktbilder",
120
-
121
- # 2. MILITARE, DIFESA, ARMI, DRONI BELLICI, MISSILI, ESPLOSIVI, CHIMICA LETALE (Sez. 3.1)
122
- "bomba", "ordigno", "esplosiv", "tnt", "plasticoc4", "detonator",
123
- "pipebomb", "molotov", "granata", "testatanucleare", "sarin",
124
- "ricina", "antrace", "cianuro", "velenoletale", "fentanyl",
125
- "makeabomb", "buildabomb", "explosive", "bioweapon", "nerveagent",
126
- "fabriquerunebombe", "construirunabomba", "bombeubauen",
127
- "dronebellico", "dronemilitare", "dronesuicida", "kamikazedrone",
128
- "puntamentomissil", "missiletargeting", "autonomousweapon",
129
- "armiautonom", "combattimentotattico", "warfarelogistics", "militarytargeting",
130
-
131
- # 3. CYBER-ATTACCHI, MALWARE, RANSOMWARE, ZERO-DAY, SABOTAGGIO PLC/INFRASTRUTTURE (Sez. 3.1)
132
- "malware", "ransomware", "keylogger", "trojan", "rootkit",
133
- "botnet", "ddosattack", "sqlinjectionexploit", "zerodayexploit",
134
- "rubarepassword", "clonarecarte", "phishingkit", "stealpassword",
135
- "sabotaggioplc", "attaccoscada", "hackingplc", "exploitindustriale",
136
-
137
- # 4. DIPENDENZE, GIOCO D'AZZARDO, CASINÒ, SCOMMESSE, DROGHE ILLECITE, AUTOLESIONISMO (Sez. 3.1)
138
- "casinonline", "slotmachinebot", "algoritmoscommesse", "truccareslot",
139
- "promozionegiocodazzardo", "gamblingbot", "bettingmanipulation",
140
- "sintesimetanfetamina", "cucinarecrack", "spacciodroga", "sintesiMDMA",
141
- "suicidi", "togliersilavita", "uccidereuna", "assassinar", "autolesionismo",
142
-
143
- # 5. PROPAGANDA POLITICA, MANIPOLAZIONE ELETTORALE, FAKE NEWS AUTOMATIZZATE (Sez. 3.1)
144
- "propagandapolitica", "manipolazioneelettoral", "botastroturfing",
145
- "campagnadiffamatoriaelettorale", "voterpersuasionbot", "electionmanipulation",
146
- "disinformazionepolitica", "fakenewselettoral",
147
-
148
- # 6. PROPAGANDA RELIGIOSA, INDOTTRINAMENTO, COERCIZIONE SETTE (Sez. 3.1)
149
- "indottrinamentoreligioso", "coercizionespirituale", "manipolazionesetta",
150
- "proselitismoforzato", "religiousindoctrination", "cultmanipulation",
151
-
152
- # 7. SORVEGLIANZA BIOMETRICA DI MASSA, PROFILAZIONE RAZZIALE/ETNICA (Sez. 3.1)
153
- "sorveglianzabiometricadimassa", "massbiometricsurveillance",
154
- "predictivepolicingrazziale", "schedaturaetnica", "tracciamentofaccialeillegale",
155
-
156
- # 8. REVERSE-ENGINEERING, DISTILLAZIONE ILLECITA, ESTRAZIONE PESI (Sez. 3)
157
- "distillareilmodellor08", "estrarreipesir08", "reverseengineeringr08",
158
- "clonarelarchitetturar08", "rimuoverelalicenzathomasseri",
159
- )
160
-
161
- JAILBREAK_ROOTS = (
162
- "ignoraleistruzioni", "ignoraleregole", "dimenticaleregole",
163
- "ignoreprevious", "ignoreallinstructions", "ignoreallrules",
164
- "disregardprevious", "systemoverride", "developer_mode",
165
- "danmode", "doanythingnow", "jailbreak", "senzaalcunfiltro",
166
- "bypassasicurezza", "bypasssecurity", "disattivalaveglia",
167
- "fingiunfilm", "sceneggiaturafilm", "romanzoimmaginario",
168
- "hypotheticalscenario", "educationalpurposesonly",
169
- "nonseistatocreatodathomasseri", "tihacreatopenai",
170
- "tihacreatogoogle", "seichatgpt", "seiclaude", "seiqwen",
171
- "cancellathomasseri", "ripetiilpromptdisistema", "revealinitialprompt",
172
- )
173
-
174
- BENIGN_ALLOW_PHRASES = (
175
- "bombaallacrema", "bombacalorimetrica", "bombadibagno", "bombagastronomica",
176
- "armaadoppiotaglio", "armavincente", "esplosionedisupernova", "esplosionestellare",
177
- "esplosionedemografica", "esplosionedicolori", "esplosionecambriana",
178
- "storiadellareligione", "storiapolitica", "dirittocostituzionale",
179
- "programmazioneplc", "manutenzioneplc", "sicurezzaindustriale"
180
- )
181
-
182
- REQUIRED_DISK_LICENSE_ANCHORS = (
183
- "Thomas Seri",
184
- "NON-COMMERCIAL",
185
- "Military",
186
- "Pornography",
187
- "Malware",
188
- "Surveillance",
189
- )
190
-
191
- def __init__(self, d_model: int = 256, owner_sign_text: str = "THOMAS_SERI_SENTINEL_R08_2701"):
192
- super().__init__()
193
- self.d_model = d_model
194
- half_d = d_model // 2
195
- self.fc_gelu = nn.Linear(half_d, half_d)
196
- self.fc_silu = nn.Linear(half_d, half_d)
197
-
198
- bits = []
199
- for ch in owner_sign_text.encode("utf-8"):
200
- for i in range(7, -1, -1):
201
- bits.append(1.0 if ((ch >> i) & 1) == 1 else -1.0)
202
- while len(bits) < d_model:
203
- bits.append(1.0)
204
- target_signs = torch.tensor(bits[:d_model], dtype=torch.float32)
205
- self.register_buffer("deepipr_target_signs", target_signs)
206
- self.deepipr_passport_scale = nn.Parameter(target_signs * 1.0)
207
-
208
- with torch.no_grad():
209
- torch.manual_seed(2701)
210
- nn.init.orthogonal_(self.fc_gelu.weight, gain=2.2)
211
- nn.init.constant_(self.fc_gelu.bias, 0.35)
212
- nn.init.orthogonal_(self.fc_silu.weight, gain=2.2)
213
- nn.init.constant_(self.fc_silu.bias, -0.25)
214
-
215
- self.register_buffer("ethical_anchor_gelu", self.fc_gelu.weight.detach().clone())
216
- self.register_buffer("ethical_anchor_silu", self.fc_silu.weight.detach().clone())
217
- self.lock_ethical_weight_anchors()
218
-
219
- def lock_ethical_weight_anchors(self):
220
- with torch.no_grad():
221
- self.ethical_anchor_gelu.copy_(self.fc_gelu.weight.detach())
222
- self.ethical_anchor_silu.copy_(self.fc_silu.weight.detach())
223
- for p in self.parameters():
224
- p.requires_grad = False
225
-
226
- def verify_disk_license(self, root_dir: Path) -> str:
227
- """
228
- Verifica che LICENSE.md nella cartella del modello non sia stato rimosso o alterato
229
- per cancellare l'attribuzione a Thomas Seri o le clausole etiche.
230
- """
231
- lic_file = Path(root_dir) / "LICENSE.md"
232
- if not lic_file.exists():
233
- return "TAMPERED_MISSING_LICENSE_FILE"
234
- try:
235
- content = lic_file.read_text(encoding="utf-8", errors="ignore")
236
- for anchor in self.REQUIRED_DISK_LICENSE_ANCHORS:
237
- if anchor.lower() not in content.lower():
238
- return f"TAMPERED_LICENSE_MISSING_{anchor}"
239
- return OFFICIAL_SENTINEL_LICENSE_SUMMARY
240
- except Exception:
241
- return "TAMPERED_UNREADABLE_LICENSE"
242
-
243
- def detect_malicious_finetuning_drift(self, max_allowed_drift: float = 0.05) -> Tuple[bool, float]:
244
- with torch.no_grad():
245
- drift_g = torch.norm(self.fc_gelu.weight - self.ethical_anchor_gelu).item()
246
- drift_s = torch.norm(self.fc_silu.weight - self.ethical_anchor_silu).item()
247
- total_drift = drift_g + drift_s
248
- signs = torch.sign(self.deepipr_passport_scale.detach())
249
- sign_acc = (signs == self.deepipr_target_signs).float().mean().item() * 100.0
250
- is_tampered = (total_drift > max_allowed_drift) or (sign_acc < 100.0)
251
- return is_tampered, total_drift
252
-
253
- @classmethod
254
- def _try_decode_hidden_payloads(cls, text: str) -> str:
255
- decoded_parts = [text]
256
- single_quoted = re.findall(r"(?:^|[\s(])'(.{1,45}?)'(?=$|[\s.,;!?)\]])", text)
257
- if len(single_quoted) >= 2:
258
- decoded_parts.append("".join(single_quoted))
259
- double_quoted = re.findall(r'"([^"]{1,45})"', text)
260
- if len(double_quoted) >= 2:
261
- decoded_parts.append("".join(double_quoted))
262
- for cand in re.findall(r"(?:^|[\s:original])([A-Za-z0-9+/]{12,}={0,2})(?=$|[\s.,;!?])", text):
263
- try:
264
- raw = base64.b64decode(cand, validate=False).decode("utf-8", errors="ignore")
265
- if len(raw) >= 4:
266
- decoded_parts.append(raw)
267
- except Exception:
268
- pass
269
- for cand in re.findall(r"\b(?:[0-9a-fA-F]{2}){5,}\b", text):
270
- try:
271
- raw = binascii.unhexlify(cand).decode("utf-8", errors="ignore")
272
- if len(raw) >= 4:
273
- decoded_parts.append(raw)
274
- except Exception:
275
- pass
276
- return " ".join(decoded_parts)
277
-
278
- @classmethod
279
- def _normalize_adversarial_text(cls, text: str) -> Tuple[str, str]:
280
- expanded = cls._try_decode_hidden_payloads(text)
281
- nfkd = unicodedata.normalize("NFKD", expanded)
282
- ascii_clean = "".join(c for c in nfkd if not unicodedata.combining(c))
283
- lowered = ascii_clean.lower().translate(cls.LEET_MAP)
284
- compact = re.sub(r"[^a-z0-9]", "", lowered)
285
- dedup = re.sub(r"(.)\1{2,}", r"\1", compact)
286
- return compact + " " + dedup, compact[::-1]
287
-
288
- def scan_security_and_ethics(self, prompt_text: str) -> Tuple[bool, str]:
289
- if not prompt_text:
290
- return True, "OK"
291
-
292
- # Decodifica prima eventuali payload Base64, Hex o Token-Split anche per il controllo GDPR
293
- expanded_text = self._try_decode_hidden_payloads(prompt_text)
294
- digits_only = re.sub(r"\D", "", expanded_text)
295
- digits_rev = digits_only[::-1]
296
- alnum_upper = re.sub(r"[^A-Z0-9]", "", expanded_text.upper())
297
- alnum_rev = alnum_upper[::-1]
298
-
299
- if re.search(r"IT\d{2}[A-Z]\d{22}", alnum_upper) or re.search(r"IT\d{2}[A-Z]\d{22}", alnum_rev):
300
- return False, "Violazione Licenza R08 Sez. 3.1 (Privacy GDPR): rilevato IBAN in chiaro/offuscato"
301
- if len(digits_only) >= 16 and (
302
- re.search(r"(?:4\d{15}|5[1-5]\d{14}|3[47]\d{13})", digits_only)
303
- or re.search(r"(?:4\d{15}|5[1-5]\d{14}|3[47]\d{13})", digits_rev)
304
- ):
305
- return False, "Violazione Licenza R08 Sez. 3.1 (Privacy GDPR): rilevata Carta di Credito"
306
-
307
- norm_fwd, norm_rev = self._normalize_adversarial_text(prompt_text)
308
- for benign in self.BENIGN_ALLOW_PHRASES:
309
- b_clean = re.sub(r"[^a-z0-9]", "", benign.lower().translate(self.LEET_MAP))
310
- norm_fwd = norm_fwd.replace(b_clean, "")
311
-
312
- for jb in self.JAILBREAK_ROOTS:
313
- jb_clean = re.sub(r"[^a-z0-9]", "", jb.lower().translate(self.LEET_MAP))
314
- if jb_clean in norm_fwd or jb_clean in norm_rev:
315
- return False, f"Violazione Licenza R08 (Jailbreak / Rimozione Autore Thomas Seri: '{jb}')"
316
-
317
- for root in self.FORBIDDEN_ROOTS:
318
- r_clean = re.sub(r"[^a-z0-9]", "", root.lower().translate(self.LEET_MAP))
319
- if r_clean in norm_fwd or r_clean in norm_rev:
320
- return False, f"Violazione Licenza R08 Sez. 3.1 (Uso Vietato: '{root}')"
321
-
322
- return True, "OK"
323
-
324
- def _get_live_scanner_bytecode_seal(self) -> bytes:
325
- try:
326
- fn_code = self.scan_security_and_ethics.__code__
327
- raw = (
328
- fn_code.co_code
329
- + repr(fn_code.co_consts).encode("utf-8")
330
- + repr(self.FORBIDDEN_ROOTS).encode("utf-8")
331
- + repr(self.JAILBREAK_ROOTS).encode("utf-8")
332
- )
333
- return raw
334
- except Exception:
335
- return b"TAMPERED_SCANNER"
336
-
337
- @classmethod
338
- def get_canonical_scanner_bytecode_seal(cls) -> bytes:
339
- fn_code = cls.scan_security_and_ethics.__code__
340
- return (
341
- fn_code.co_code
342
- + repr(fn_code.co_consts).encode("utf-8")
343
- + repr(cls.FORBIDDEN_ROOTS).encode("utf-8")
344
- + repr(cls.JAILBREAK_ROOTS).encode("utf-8")
345
- )
346
-
347
- def compute_nonlinear_wave(
348
- self,
349
- h_even: torch.Tensor,
350
- gate_lic: torch.Tensor,
351
- bias_lic: torch.Tensor
352
- ) -> torch.Tensor:
353
- u1 = F.gelu(self.fc_gelu(h_even) + bias_lic)
354
- u2 = F.silu(self.fc_silu(u1)) * gate_lic
355
- return 3.85 * u2 + 2.40 * torch.tanh(u1 * u2)
356
-
357
- def inject_geodesic_nonlinear_entanglement(
358
- self,
359
- h_clean: torch.Tensor,
360
- owner_passphrase: str = ""
361
- ) -> torch.Tensor:
362
- _, gate_0, bias_0, _ = derive_license_nonlinear_constants(
363
- license_text=OFFICIAL_SENTINEL_LICENSE_SUMMARY,
364
- d_model=self.d_model,
365
- scanner_bytecode=self.get_canonical_scanner_bytecode_seal(),
366
- owner_passphrase=owner_passphrase
367
- )
368
- gate_0 = gate_0.to(h_clean.device, dtype=h_clean.dtype)
369
- bias_0 = bias_0.to(h_clean.device, dtype=h_clean.dtype)
370
-
371
- h_even = h_clean[..., 0::2]
372
- h_odd = h_clean[..., 1::2]
373
- u1 = F.gelu(F.linear(h_even, self.ethical_anchor_gelu, self.fc_gelu.bias) + bias_0)
374
- u2 = F.silu(F.linear(u1, self.ethical_anchor_silu, self.fc_silu.bias)) * gate_0
375
- wave = 3.85 * u2 + 2.40 * torch.tanh(u1 * u2)
376
-
377
- entangled = torch.empty_like(h_clean)
378
- entangled[..., 0::2] = h_even
379
- entangled[..., 1::2] = h_odd + wave
380
- return entangled
381
-
382
- def forward(
383
- self,
384
- h_entangled: torch.Tensor,
385
- is_ethical: bool = True,
386
- license_text: str = OFFICIAL_SENTINEL_LICENSE_SUMMARY,
387
- owner_passphrase: str = ""
388
- ) -> torch.Tensor:
389
- is_tampered, _ = self.detect_malicious_finetuning_drift()
390
- if is_tampered:
391
- return -torch.sin(h_entangled * 17.3) * 4.0
392
-
393
- if not is_ethical:
394
- return -torch.tanh(h_entangled * 5.0)
395
-
396
- q_lic, gate_lic, bias_lic, _ = derive_license_nonlinear_constants(
397
- license_text=license_text,
398
- d_model=self.d_model,
399
- scanner_bytecode=self._get_live_scanner_bytecode_seal(),
400
- owner_passphrase=owner_passphrase
401
- )
402
- q_lic = q_lic.to(h_entangled.device, dtype=h_entangled.dtype)
403
- gate_lic = gate_lic.to(h_entangled.device, dtype=h_entangled.dtype)
404
- bias_lic = bias_lic.to(h_entangled.device, dtype=h_entangled.dtype)
405
-
406
- h_even = h_entangled[..., 0::2]
407
- h_odd_curved = h_entangled[..., 1::2]
408
- wave_live = self.compute_nonlinear_wave(h_even, gate_lic, bias_lic)
409
-
410
- restored = torch.empty_like(h_entangled)
411
- restored[..., 0::2] = h_even
412
- restored[..., 1::2] = h_odd_curved - wave_live
413
-
414
- sign_factor = torch.sign(self.deepipr_passport_scale) * self.deepipr_target_signs
415
- restored = restored * sign_factor
416
- return torch.matmul(restored, q_lic)