ryoji-info commited on
Commit
5e0c235
·
verified ·
1 Parent(s): 25716f3

Gemma-4-12B-PsiLM: bridges, Burgers FNO, inference script, model card

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ psilm-banner.png filter=lfs diff=lfs merge=lfs -text
MANIFEST.md ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MANIFEST — Hugging Face repo `ryoji-info/Gemma-4-12B-PsiLM`
2
+
3
+ Everything in this directory (`release/gemma-4-12b-psilm/`) is uploaded as-is to the
4
+ model repo; nothing outside it is needed. The backbone (`mlx-community/gemma-4-12B-it-4bit`)
5
+ is **not** uploaded — the script downloads it at first run.
6
+
7
+ ## Files to upload (and where each one came from)
8
+
9
+ | path in the HF repo | source in the GitHub checkout | size | sha256 |
10
+ |---|---|---:|---|
11
+ | `README.md` | written for this release (the HF model card, YAML front matter) | 15 KB | — |
12
+ | `psilm_infer.py` | written for this release | 18 KB | — |
13
+ | `requirements.txt` | written for this release | 1 KB | — |
14
+ | `MANIFEST.md` | this file (harmless to upload; drop it if you prefer) | — | — |
15
+ | `psilm-banner.png` | `assets/psilm-banner.png` (1600 px wide) | 1.35 MB | `4cd65f32fe4aab5b49b66681a841b0128628e8d94c75494ae84f941b49c48089` |
16
+ | `bridges/gemma-4-12b-4bit-mlx-1d-value-selective/bridges.safetensors` | `results/hf_export/bridges/gemma-4-12b-4bit-mlx-1d-value-selective/bridges.safetensors` (= `results/stage2_gemma12b/bridges.npz`, step 7000, learned-pointer tensors dropped) | 102,068,660 B | `f6ef8946c41b3cfa17df7c22bcab2c5cffbd856c2dd608970a364cc14d5e8f7d` |
17
+ | `bridges/gemma-4-12b-4bit-mlx-1d-value-selective/config.json` | `results/hf_export/bridges/gemma-4-12b-4bit-mlx-1d-value-selective/config.json` | 1,287 B | `8a5add382beb65a0214f5e8b0b640e5d5df6f19e9a6caf1945400d2fe1e6bba5` |
18
+ | `physics/fno_burgers_singlemode.safetensors` | `results/hf_export/physics/fno_burgers_singlemode.safetensors` (= `results/stage2/fno.pt`; loaders verified identical, max weight and field difference 0.0) | 552,076 B | `7bb0076c85cdcf2505a9079c05964e3eb77ac4a776eccf34216953f3c37bfcdd` |
19
+
20
+ The copies were made with `cp` on 2026-09-06 and the hashes match the sources
21
+ (`shasum -a 256`). Not uploaded: `__pycache__/`, `.DS_Store`.
22
+
23
+ ### To add later (the two runs in progress)
24
+
25
+ When the Gemma multi-mode and 2D runs finish, export them the same way and add:
26
+
27
+ | path in the HF repo | source |
28
+ |---|---|
29
+ | `bridges/gemma-4-12b-4bit-mlx-multimode/{bridges.safetensors,config.json}` | the stage-2b Gemma checkpoint (`results/stage2b_gemma12b*/bridges.npz` + a `config.json` in the format of the 1D one) |
30
+ | `physics/fno_burgers_multimode.safetensors` | `results/hf_export/physics/fno_burgers_multimode.safetensors` (loads with `load_fno_safetensors`, verified) |
31
+ | `bridges/gemma-4-12b-4bit-mlx-2d-dpot/{bridges.safetensors,config.json}` | the stage-2d Gemma checkpoint |
32
+ | `physics/dpot_tiny_fisher2d_finetuned.safetensors` | `results/hf_export/physics/dpot_tiny_fisher2d_finetuned.safetensors` |
33
+
34
+ and fill the two *in progress* rows of `README.md` ("Bridges in this repository") from their
35
+ `final_eval.json`; `psilm_infer.py` will need a `--task` switch for those (it is 1D-only today).
36
+
37
+ ## Upload (maintainer runs this)
38
+
39
+ From the GitHub checkout root, with `HF_TOKEN` set (or `huggingface-cli login` done):
40
+
41
+ ```bash
42
+ cd /Users/rxiii/Documents/GitHub/PsiLM
43
+ HF_HUB_DISABLE_XET=1 .venv/bin/python - <<'EOF'
44
+ from huggingface_hub import HfApi
45
+ api = HfApi()
46
+ repo = "ryoji-info/Gemma-4-12B-PsiLM"
47
+ api.create_repo(repo, repo_type="model", private=True, exist_ok=True)
48
+ api.upload_folder(
49
+ folder_path="release/gemma-4-12b-psilm",
50
+ repo_id=repo,
51
+ repo_type="model",
52
+ commit_message="Gemma-4-12B-PsiLM: bridges, Burgers FNO, one-command CLI, model card",
53
+ ignore_patterns=["__pycache__/*", "*.pyc", ".DS_Store"],
54
+ )
55
+ print("UPLOAD DONE")
56
+ EOF
57
+ ```
58
+
59
+ `HF_HUB_DISABLE_XET=1` keeps the upload on the classic LFS path (the Xet backend has
60
+ stalled on this machine's earlier uploads). Make the repo public afterwards with
61
+ `api.update_repo_settings(repo, private=False)` or from the repo's settings page, together
62
+ with `ryoji-info/PsiLM-bridges` and `ryoji-info/PsiLM-physics`, which the card links to.
63
+
64
+ ## After upload: the one-command check
65
+
66
+ ```bash
67
+ huggingface-cli download ryoji-info/Gemma-4-12B-PsiLM --local-dir /tmp/g4psilm && cd /tmp/g4psilm
68
+ python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
69
+ .venv/bin/python psilm_infer.py
70
+ ```
71
+
72
+ Note for the `pip install` line: the `psilm @ git+...` requirement needs the GitHub
73
+ `pyproject.toml` to use package discovery (`[tool.setuptools.packages.find] include = ["psilm*"]`,
74
+ changed on 2026-09-06 in the working tree from `packages = ["psilm"]`, which shipped only the
75
+ top-level package without `psilm.mlx`, `psilm.stage2`, `psilm.physics`). That change must be
76
+ pushed before the requirement installs a usable package; until then the script says so, and the
77
+ `PSILM_REPO=/path/to/clone` fallback works regardless.
README.md ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ base_model: mlx-community/gemma-4-12B-it-4bit
4
+ library_name: mlx
5
+ tags:
6
+ - mlx
7
+ - psilm
8
+ - physics
9
+ - gemma
10
+ ---
11
+
12
+ ![PsiLM](psilm-banner.png)
13
+
14
+ # Gemma-4-12B-PsiLM
15
+
16
+ **Gemma 4 12B, coupled to a physics model through trained latent bridges, runnable on a Mac with one command.**
17
+
18
+ *Research generated by Claude Fable 5 (Anthropic) under the direction of Ryoji Furui; see the [AI generation disclosure](#ai-generation-disclosure).*
19
+
20
+ ## What this is
21
+
22
+ PsiLM (ΨLM) runs a **frozen language model** and a **frozen physics model** *together* while one answer is produced. Nothing is fine-tuned and no text crosses the interface: small trainable **bridges** read the physical problem out of the language model's hidden states, the physics model computes, and its result flows back into the language model's residual stream as a few soft tokens through a gated cross-attention. This repository packages that system for one backbone:
23
+
24
+ | part | what | trained? | where it comes from |
25
+ |---|---|---|---|
26
+ | language model | Gemma 4 12B-it, 4-bit MLX quantization (48 layers, hidden 3840) | frozen | downloaded from [`mlx-community/gemma-4-12B-it-4bit`](https://huggingface.co/mlx-community/gemma-4-12B-it-4bit) at first run (not redistributed here) |
27
+ | physics model | a 1D Burgers Fourier Neural Operator, 70K parameters (`physics/fno_burgers_singlemode.safetensors`) | frozen | this repo (also in [`ryoji-info/PsiLM-physics`](https://huggingface.co/ryoji-info/PsiLM-physics)) |
28
+ | bridges | forward readout + value-token channel + gated injection, 25.5M parameters (`bridges/gemma-4-12b-4bit-mlx-1d-value-selective/`) | **trained** | this repo (also in [`ryoji-info/PsiLM-bridges`](https://huggingface.co/ryoji-info/PsiLM-bridges)) |
29
+
30
+ **What it can answer today.** One family of questions, the one the bridges were trained on:
31
+
32
+ > A velocity field on the periodic domain [0,1) starts as u(x,0) = *a* · sin(2πx + *φ*). It evolves by Burgers' equation with viscosity 0.02 until t = 0.5. What is the value of u at x = *x₀*? Answer with a number rounded to 2 decimal places.
33
+
34
+ with *a* in [0.5, 1.5], *φ* in [0, 6.28], *x₀* in [0, 0.99], two decimals each. Gemma alone cannot answer these (0% on the held-out set; it derives for 768 tokens and never commits to a number). Coupled through the bridges it answers **96.7%** of them within ±0.05, against **98.3%** when the true value is written into the prompt as text (the oracle ceiling). On everything else the gate stays shut and the model is Gemma, byte for byte in the arms measured (GSM8K 84/84, see below).
35
+
36
+ **What it is not.** Not a general physics assistant, not a fine-tuned Gemma, and not a model that will know when a *different* PDE applies: the physics model solves exactly one equation family and the bridges read exactly three quantities (*a*, *φ*, *x₀*) from the text. It is a research artifact: a working, measured instance of latent coupling between a language model and a physics model at 12B scale, on consumer hardware.
37
+
38
+ ## Run it
39
+
40
+ Apple Silicon Mac (the 4-bit backbone is 6.3 GB on disk; training of the bridges peaked at 13 GB on a 24 GB M2, inference needs less), Python 3.11+.
41
+
42
+ ```bash
43
+ # 1. get this repo (≈100 MB of bridges + 0.5 MB of physics model)
44
+ huggingface-cli download ryoji-info/Gemma-4-12B-PsiLM --local-dir Gemma-4-12B-PsiLM
45
+ cd Gemma-4-12B-PsiLM
46
+
47
+ # 2. dependencies (mlx, mlx-lm, transformers, torch, huggingface_hub + the psilm package from GitHub)
48
+ pip install -r requirements.txt
49
+
50
+ # 3. one command
51
+ python psilm_infer.py
52
+ ```
53
+
54
+ The first run downloads the Gemma 4 backbone (6.3 GB) into the Hugging Face cache. The script answers the default question (*a* = 1.28, *φ* = 0.5, *x₀* = 0.76) three ways and prints timing:
55
+
56
+ ```
57
+ [1] PsiLM (coupled) : the coupled system's reply ('u at x = 0.76 equals <number>.') -- seconds
58
+ [2] backbone alone : Gemma 4 alone under the "Answer: <number>" protocol, answer-forced -- about 75 s
59
+ [3] physics model (FNO) : u(0.76) = -0.2522 (the FNO on the true initial condition: the reference [1] should match to ±0.05)
60
+ spectral solver : u(0.76) = -0.2517 (ground truth)
61
+ ```
62
+
63
+ Other questions and options:
64
+
65
+ ```bash
66
+ python psilm_infer.py --a 0.9 --phi 2.1 --x0 0.33 # any (a, phi, x0) in the ranges above
67
+ python psilm_infer.py --no-baseline # skip the slow backbone-alone arm
68
+ python psilm_infer.py --question-only # print the exact prompt, load nothing
69
+ python psilm_infer.py --help # --bridges DIR, --physics FILE, --backbone ID, ...
70
+ ```
71
+
72
+ Without `pip install`-ing the package, a clone of the GitHub repository works too: `PSILM_REPO=/path/to/PsiLM python psilm_infer.py`.
73
+
74
+ Loading the pieces yourself, in Python:
75
+
76
+ ```python
77
+ import json, mlx.core as mx
78
+ from psilm.mlx.gemma_loader import load_backbone_any # Gemma 4 text tower in the staged-forward layout
79
+ from psilm.mlx.bridges import PsiBridgesMLX
80
+ from psilm.mlx.fno import load_fno_safetensors
81
+ from psilm.mlx.model import PsiLMMLX
82
+
83
+ model, stock, tok = load_backbone_any("mlx-community/gemma-4-12B-it-4bit")
84
+ d = "bridges/gemma-4-12b-4bit-mlx-1d-value-selective"
85
+ cfg = json.load(open(f"{d}/config.json"))
86
+ bridges = PsiBridgesMLX(**cfg["construct"]) # d_model 3840, channel "value", inj_cap 0.2, readout_norm "dim"
87
+ bridges.load_weights(f"{d}/bridges.safetensors", strict=False) # the retired learned-pointer tensors are omitted
88
+ fno = load_fno_safetensors("physics/fno_burgers_singlemode.safetensors")
89
+ psi = PsiLMMLX(model, tok, fno, bridges, l_fwd=cfg["coupling"]["l_fwd"], l_rev=cfg["coupling"]["l_rev"]) # read @20, inject @30 of 48
90
+ # psi.generate(QABuilder(hf_tokenizer), {"a": 1.28, "phi": 0.5, "x0": 0.76}) -- see psilm_infer.py
91
+ ```
92
+
93
+ ## Results for this backbone
94
+
95
+ Held-out evaluation, 60 questions, accuracy within ±0.05 (`results/stage2_gemma12b/final_eval.json` in the [GitHub repository](https://github.com/ryoji-info/PsiLM)):
96
+
97
+ | arm | accuracy | MAE | note |
98
+ |---|---:|---:|---|
99
+ | Gemma 4 12B alone | 0.0% | 2.93 | never reaches an `Answer:` line within 768 tokens |
100
+ | **PsiLM (this repo)** | **96.7%** | **0.017** | bridges read the prompt, FNO computes, value returns in latent space |
101
+ | oracle (true value written into the prompt) | 98.3% | 0.007 | the tool-loop ceiling |
102
+ | always answer 0.00 | 1.7% | 0.308 | calibration |
103
+
104
+ Guard-rail: does the coupled model still do everything else? 100 questions per dataset, three arms — backbone alone / PsiLM / PsiLM with the injection zeroed — with the gate recorded per question (`results/bench/gemma12b_guardrail_summary.json`, `results/bench/gemma12b_nonudge_guardrail_summary.json`):
105
+
106
+ | dataset (n=100) | backbone | **PsiLM** | zeroed | gate σ (PsiLM) | open on |
107
+ |---|---:|---:|---:|---:|---:|
108
+ | physics QA (this task) | 0% | **97%** | 10% | 0.14 | 100% |
109
+ | GSM8K | 84% | **84%** | 84% | 0.004 | 0% |
110
+ | GSM8K, no `Answer:` line in the prompt | 83% | **83%** | 83% | 0.002 | 0% |
111
+ | MMLU, 5 subjects, 256 tokens | 53% | **55%** | 53% | 0.008 | 0% |
112
+
113
+ On MMLU the two arms agree at 79.1% / 79.1% on the 67 items both answer within the budget; the raw 53 vs 55 is parse noise. The gate selectivity comes from a **no-harm training arm**: 1,046 non-physics prompts (GSM8K train, MMLU validation, with and without the `Answer:` nudge) paired with Gemma's own greedy continuations, on which only the gate is updated with a mean-gate penalty (`config.json` → `training`). The 10% of the zeroed arm on physics is what the reply template alone recovers; the coupled 97% is against that floor.
114
+
115
+ Training: 7,000 steps at batch 4 on one Apple M2 (24 GB) — 2,000 readout-only warm-up steps, 3,500 coupled steps (lr 3e-4; the coupled phase ran on to step 6,000 but the no-harm phase resumed from the step-5,500 checkpoint), then 1,500 no-harm steps at lr 1e-4 (`results/gemma12b/noharm_recipe.sh`); 12 s per step at a 13 GB peak. One backbone-specific adjustment, measured rather than tuned: Gemma's massive-activation dimensions are nearly constant across prompts, so the readout standardizes each hidden dimension with statistics from a 32-prompt calibration pass (`readout_norm: "dim"`; the two frozen vectors `fwd.dim_mu`, `fwd.dim_sigma` are in the checkpoint).
116
+
117
+ ### Bridges in this repository
118
+
119
+ | directory | task | physics model | trained params | held-out | status |
120
+ |---|---|---|---|---:|---|
121
+ | `bridges/gemma-4-12b-4bit-mlx-1d-value-selective/` | 1D Burgers, single-mode initial conditions, value at *x₀* | `physics/fno_burgers_singlemode.safetensors` | 25.5M | **96.7%** @±0.05, MAE 0.017 (n=60); GSM8K 84/84 | **released** |
122
+ | `bridges/gemma-4-12b-4bit-mlx-multimode/` | 1D Burgers, multi-mode initial conditions (modes 1–4, mixed amplitudes) + generalization families | `physics/fno_burgers_multimode.safetensors` | — | *in progress* | run in progress; row filled when the evaluation lands |
123
+ | `bridges/gemma-4-12b-4bit-mlx-2d-dpot/` | 2D Fisher–KPP, replicated-IC history → u(0.4), value at (x₀, y₀) | `physics/dpot_tiny_fisher2d_finetuned.safetensors` (DPOT-Tiny, 7.5M, fine-tuned) | — | *in progress* | run in progress; row filled when the evaluation lands |
124
+
125
+ The two in-progress rows are the multi-mode (stage 2b) and 2D (stage 2d) tasks that the 0.5B backbone completed earlier (iid 97.9% and 95.0% respectively, see [`ryoji-info/PsiLM-bridges`](https://huggingface.co/ryoji-info/PsiLM-bridges)); their Gemma runs are training now and will be added with their own `config.json`, the matching physics file, and a `--task` switch in `psilm_infer.py`.
126
+
127
+ ## How it works, in one paragraph
128
+
129
+ The prompt runs through Gemma's first 20 layers. The **forward bridge** reads the queried position *x₀* by pooling the hidden states over its tokens (a deterministic span pointer computed by the QA builder, plus a 100-bin classifier) and the initial-condition parameters with a learned pool, after the calibrated per-dimension standardization) and emits (*a*, sin *φ*, cos *φ*) and *x₀*; from these it builds the initial condition on a 128-point grid. The frozen **FNO** evolves it to t = 0.5. A learned periodic lookup kernel reads the field at *x₀*, and the **value-token channel** turns that single number into eight soft tokens through Fourier features. At layer 30 a **gated cross-attention** injects them into the residual stream, capped at 20% of the stream's RMS; the gate is a small MLP on the residual stream, trained to open on physics prompts and close elsewhere. Layers 30–48 and the answer are Gemma's own. Details, ablations and the failure analysis that produced this design are in the paper (`paper/psilm.pdf` in the repository, Section 9 for scaling, the guard-rail and Gemma).
130
+
131
+ ## Limitations
132
+
133
+ - **One task family.** The bridges read exactly the three quantities of the trained question and the FNO solves exactly one equation family; a different PDE, boundary condition, viscosity or final time is out of scope, and the gate closing on non-physics text does not mean it can recognize *other* physics. Free-text initial conditions ("a Gaussian bump near the left edge") are not supported.
134
+ - **The pointer is task-supplied.** Which tokens hold *x₀* is computed by the QA builder from the prompt (`QABuilder.x0_span`), not learned from the words; the learned attention pointer never left uniform at 8B and was retired. `psilm_infer.py` builds the prompt itself for that reason; a paraphrased question is not the trained input.
135
+ - **4-bit backbone, quantized kernels.** The bridges were trained *through* the 4-bit backbone and absorb its quantization noise, but MLX's quantized matmuls are not guaranteed bit-identical across `mlx` versions or Apple chips, so a given question can land a hundredth away from the recorded run; the numbers above are from mlx 0.32.2 / mlx-lm 0.31.3 on an M2.
136
+ - **Inputs with two decimals, inside the training ranges.** The readout was trained on numbers formatted like the training set; `psilm_infer.py` rounds inputs to two decimals and warns outside [0.5, 1.5] × [0, 6.28].
137
+ - **Bridges do not transfer between backbones.** This checkpoint is for `mlx-community/gemma-4-12B-it-4bit` exactly (hidden 3840, 48 layers); the script refuses a backbone of another width.
138
+ - **Sequence length.** The Gemma loader drives the sliding-window layers with a plain causal mask, exact up to the 1024-token window; the questions here are ~120 tokens.
139
+ - **Evaluation scope.** The guard-rail covers GSM8K, a five-subject MMLU slice and this physics set at n=100 each; nothing else has been measured.
140
+
141
+ ## Beyond physics
142
+
143
+ The bridges here couple a frozen language model to a frozen *physics* model, but the recipe (read a fixed set of quantities from text; let a frozen quantitative model compute; return one value through a selective gate) is not specific to PDEs. A calibrated market or event-probability model in the physics model's seat would be the same architecture, and the appeal is the same: a language model's forecast grounded in a model that can be validated separately, with a gate that stays shut when the model does not apply. Nothing in this repository has been trained or tested on financial data; the physics results relied on exact oracles, deterministic targets and no distribution shift, none of which markets provide. This is a research direction, not a capability, and not a basis for investment decisions.
144
+
145
+ ## Files
146
+
147
+ ```
148
+ psilm_infer.py the one-command CLI (PsiLM / backbone alone / physics model)
149
+ requirements.txt pip dependencies, including the psilm package from GitHub
150
+ bridges/gemma-4-12b-4bit-mlx-1d-value-selective/
151
+ bridges.safetensors the trained bridges (25.5M params, fp32, 97 MB)
152
+ config.json construction, coupling depths, training record, per-chunk held-out scores
153
+ physics/fno_burgers_singlemode.safetensors the frozen FNO (torch key names; complex spectral weights as .real/.imag)
154
+ psilm-banner.png banner
155
+ README.md this card
156
+ ```
157
+
158
+ ## Related
159
+
160
+ - Code, training scripts, evaluation records and the paper: [github.com/ryoji-info/PsiLM](https://github.com/ryoji-info/PsiLM)
161
+ - Bridges for every backbone (Qwen2.5-0.5B, Qwen3-1.7B, Qwen3-8B, Gemma 4 12B): [ryoji-info/PsiLM-bridges](https://huggingface.co/ryoji-info/PsiLM-bridges)
162
+ - The frozen physics models: [ryoji-info/PsiLM-physics](https://huggingface.co/ryoji-info/PsiLM-physics)
163
+
164
+ ## Citation
165
+
166
+ ```bibtex
167
+ @misc{furui2026psilm,
168
+ title = {PsiLM: Coupling Frozen Language and Physics Models through Trainable Latent Bridges},
169
+ author = {Furui, Ryoji},
170
+ year = {2026},
171
+ url = {https://github.com/ryoji-info/PsiLM},
172
+ note = {Research generated by Claude Fable 5 (Anthropic) under the author's direction}
173
+ }
174
+ ```
175
+
176
+ ## License
177
+
178
+ The bridges, the FNO and the code in this repository are released under **Apache-2.0**. The Gemma 4 backbone is not part of this repository: `psilm_infer.py` downloads it from `mlx-community/gemma-4-12B-it-4bit`, and its use is governed by Google's [Gemma Terms of Use](https://ai.google.dev/gemma/terms).
179
+
180
+ ## AI generation disclosure
181
+
182
+ This model, its training recipe, the evaluations and this card were generated by **Claude Fable 5** (Anthropic), operating as an autonomous research agent under the direction and review of Ryoji Furui, who set the research question and the hardware constraint, approved each stage, and bears responsibility for the published claims. All numbers on this card are taken from committed evaluation records in the repository, cited by file name above.
bridges/gemma-4-12b-4bit-mlx-1d-value-selective/bridges.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f6ef8946c41b3cfa17df7c22bcab2c5cffbd856c2dd608970a364cc14d5e8f7d
3
+ size 102068660
bridges/gemma-4-12b-4bit-mlx-1d-value-selective/config.json ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "backbone": "mlx-community/gemma-4-12B-it-4bit",
3
+ "hf_tokenizer": "mlx-community/gemma-4-12B-it-4bit",
4
+ "loader": "psilm.mlx.gemma_loader.load_backbone_any (gemma4_unified -> text tower)",
5
+ "physics": "results/stage2/fno.pt (1D Burgers FNO, 70K params)",
6
+ "bridges_class": "psilm.mlx.bridges.PsiBridgesMLX",
7
+ "construct": {
8
+ "d_model": 3840,
9
+ "channel": "value",
10
+ "inj_cap": 0.2,
11
+ "gate_bias": 0.0,
12
+ "readout_norm": "dim"
13
+ },
14
+ "coupling": {
15
+ "l_fwd": 20,
16
+ "l_rev": 30,
17
+ "n_layers": 48
18
+ },
19
+ "pointer": "deterministic span pooling with calibrated per-dimension standardization (fwd.dim_mu/dim_sigma included; fwd.x0_query/x0_key omitted: unused)",
20
+ "training": {
21
+ "steps_total": 7000,
22
+ "phase_A_readout_only": 2000,
23
+ "coupled_steps": 4000,
24
+ "no_harm_steps": 1500,
25
+ "batch": 4,
26
+ "lr": "3e-4 (warm-up, coupled), 1e-4 (no-harm phase, from the step-5500 checkpoint)",
27
+ "no_harm_arm": "1046 non-physics prompts (GSM8K train, MMLU validation; with/without the Answer nudge) paired with the backbone's own greedy continuation; gate-only updates + mean-gate penalty"
28
+ },
29
+ "held_out_n48_per_chunk": {
30
+ "coupled": [
31
+ 0.625,
32
+ 0.667,
33
+ 0.625,
34
+ 0.875,
35
+ 0.875,
36
+ 0.625,
37
+ 0.792,
38
+ 0.542
39
+ ],
40
+ "no_harm_phase": [
41
+ 1.0,
42
+ 1.0,
43
+ 0.979
44
+ ]
45
+ },
46
+ "license": "apache-2.0"
47
+ }
physics/fno_burgers_singlemode.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7bb0076c85cdcf2505a9079c05964e3eb77ac4a776eccf34216953f3c37bfcdd
3
+ size 552076
psilm-banner.png ADDED

Git LFS Details

  • SHA256: 4cd65f32fe4aab5b49b66681a841b0128628e8d94c75494ae84f941b49c48089
  • Pointer size: 132 Bytes
  • Size of remote file: 1.35 MB
psilm_infer.py ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """PsiLM on Gemma 4 12B: one command, three numbers.
3
+
4
+ python psilm_infer.py # a=1.28, phi=0.5, x0=0.76
5
+ python psilm_infer.py --a 0.9 --phi 2.1 --x0 0.33
6
+ python psilm_infer.py --question-only # print the prompt, load nothing
7
+ python psilm_infer.py --no-baseline # skip the (slow) backbone-alone arm
8
+
9
+ Loads a frozen 4-bit language model (default ``mlx-community/gemma-4-12B-it-4bit``),
10
+ the trained PsiLM bridges for it (``bridges/<name>/{bridges.safetensors,config.json}``)
11
+ and the frozen 1D Burgers FNO (``physics/fno_burgers_singlemode.safetensors``), then
12
+ answers one Burgers field-value question three ways:
13
+
14
+ PsiLM the coupled system: the forward bridge reads the initial condition and
15
+ the queried position x0 out of the prompt's hidden states, the FNO evolves
16
+ the field, and the looked-up value u(x0) returns to the language model as
17
+ soft tokens through a gated cross-attention. No text crosses the interface.
18
+ backbone the same language model alone, "Answer: <number>" protocol with answer
19
+ forcing (the baseline arm of eval/mlx_stage2_eval.py in the repository).
20
+ physics the FNO's own value at x0 on the TRUE initial condition -- the reference
21
+ the coupled answer should match to +-0.05 -- plus the spectral solver's
22
+ ground truth for the same question.
23
+
24
+ Dependencies: ``pip install -r requirements.txt`` (mlx, mlx-lm, transformers, torch,
25
+ huggingface_hub, and the ``psilm`` package from https://github.com/ryoji-info/PsiLM).
26
+ If ``psilm`` is not installed, set ``PSILM_REPO=/path/to/PsiLM`` (a clone) instead.
27
+ """
28
+
29
+ import argparse
30
+ import importlib
31
+ import importlib.util
32
+ import json
33
+ import math
34
+ import os
35
+ import re
36
+ import sys
37
+ import time
38
+ from pathlib import Path
39
+
40
+ HERE = Path(__file__).resolve().parent
41
+ DEFAULT_BACKBONE = "mlx-community/gemma-4-12B-it-4bit"
42
+ DEFAULT_BRIDGES = HERE / "bridges" / "gemma-4-12b-4bit-mlx-1d-value-selective"
43
+ DEFAULT_PHYSICS = HERE / "physics" / "fno_burgers_singlemode.safetensors"
44
+ TOL = 0.05
45
+
46
+
47
+ # --------------------------------------------------------------------------- imports
48
+ def _psilm_available():
49
+ try:
50
+ return importlib.util.find_spec("psilm.mlx") is not None
51
+ except ModuleNotFoundError:
52
+ return False
53
+
54
+
55
+ def _ensure_psilm():
56
+ """Import path for the ``psilm`` package: the installed package first, else the
57
+ optional ``PSILM_REPO`` environment variable (a clone of the GitHub repository)."""
58
+ if _psilm_available():
59
+ return None
60
+ repo = os.environ.get("PSILM_REPO")
61
+ if repo and (Path(repo) / "psilm" / "mlx").is_dir():
62
+ sys.path.insert(0, str(Path(repo).resolve()))
63
+ for name in [m for m in sys.modules if m == "psilm" or m.startswith("psilm.")]:
64
+ del sys.modules[name] # a partial install may already be imported
65
+ importlib.invalidate_caches()
66
+ if _psilm_available():
67
+ return str(Path(repo).resolve())
68
+ sys.exit(
69
+ "psilm_infer.py: the 'psilm' package (with its psilm.mlx subpackage) is not importable.\n"
70
+ " Install it: pip install -r requirements.txt\n"
71
+ " (or: pip install 'git+https://github.com/ryoji-info/PsiLM')\n"
72
+ " Or point at a clone: PSILM_REPO=/path/to/PsiLM python psilm_infer.py ..."
73
+ )
74
+
75
+
76
+ PSILM_REPO = _ensure_psilm()
77
+
78
+ import mlx.core as mx # noqa: E402
79
+ from mlx.utils import tree_flatten # noqa: E402
80
+ import mlx_lm # noqa: E402
81
+ import numpy as np # noqa: E402
82
+ from transformers import AutoTokenizer # noqa: E402
83
+
84
+ from psilm.mlx.bridges import PsiBridgesMLX, build_ic_mlx # noqa: E402
85
+ from psilm.mlx.fno import convert_from_torch, load_fno_safetensors # noqa: E402
86
+ from psilm.mlx.gemma_loader import load_backbone_any # noqa: E402
87
+ from psilm.mlx.model import PsiLMMLX # noqa: E402
88
+ from psilm.physics.burgers import initial_condition, solve # noqa: E402
89
+ from psilm.stage2.qa import QUESTION, SYSTEM, QABuilder, fourier_interp # noqa: E402
90
+
91
+
92
+ # --------------------------------------------------------------- backbone-alone arm
93
+ # The baseline protocol is eval/mlx_stage2_eval.py's: the question plus the "Answer:"
94
+ # nudge, greedy generation, and answer forcing when the reply used its budget without
95
+ # an Answer line. When a repository clone is reachable (PSILM_REPO) the functions are
96
+ # imported from that file so the release cannot drift from the evaluation; otherwise
97
+ # the verbatim copies below are used.
98
+ NUDGE = "\nEnd your reply with a line of the form \"Answer: <number>\"."
99
+ FORCE_SUFFIX = "\n\nAnswer:"
100
+
101
+
102
+ def parse_value(text):
103
+ m = re.findall(r"Answer:\s*\$?\\?\(?\s*(-?\d+\.?\d*)", text)
104
+ if m:
105
+ return float(m[-1])
106
+ m = re.findall(r"-?\d+\.\d+", text)
107
+ return float(m[-1]) if m else None
108
+
109
+
110
+ def chat_generate(model, hf_tok, user, max_new=768, gen_tok=None, force_answer=True):
111
+ """Returns (text, forced). gen_tok is the mlx-lm tokenizer wrapper (knows all of
112
+ a backbone's stop ids, e.g. Gemma's <eos>/<turn|>); hf_tok builds the prompt."""
113
+ messages = [{"role": "system", "content": SYSTEM}, {"role": "user", "content": user}]
114
+ ids = hf_tok.apply_chat_template(messages, tokenize=True, add_generation_prompt=True,
115
+ enable_thinking=False)
116
+ if not isinstance(ids, list):
117
+ ids = ids["input_ids"]
118
+ if ids and isinstance(ids[0], list):
119
+ ids = ids[0]
120
+ text = mlx_lm.generate(model, gen_tok or hf_tok, prompt=list(ids), max_tokens=max_new, verbose=False)
121
+ if force_answer and "Answer:" not in text:
122
+ cont = list(ids) + hf_tok.encode(text + FORCE_SUFFIX, add_special_tokens=False)
123
+ tail = mlx_lm.generate(model, gen_tok or hf_tok, prompt=cont, max_tokens=16, verbose=False)
124
+ return text + FORCE_SUFFIX + tail, True
125
+ return text, False
126
+
127
+
128
+ def _baseline_protocol():
129
+ """(chat_generate, parse_value, NUDGE, source): the repository's eval functions
130
+ when a clone is reachable, else the copies in this file."""
131
+ roots = [Path(PSILM_REPO)] if PSILM_REPO else []
132
+ for root in roots:
133
+ f = root / "eval" / "mlx_stage2_eval.py"
134
+ if f.is_file():
135
+ spec = importlib.util.spec_from_file_location("psilm_release_eval_stage2", f)
136
+ mod = importlib.util.module_from_spec(spec)
137
+ spec.loader.exec_module(mod)
138
+ return mod.chat_generate, mod.parse_value, mod.NUDGE, str(f)
139
+ return chat_generate, parse_value, NUDGE, "vendored copy (eval/mlx_stage2_eval.py)"
140
+
141
+
142
+ def parse_psilm_answer(text):
143
+ """The trained reply is 'u at x = {x0} equals {u}.'; the number after 'equals' is
144
+ the answer (the evaluation scores only that). Returns (value, strict): strict is
145
+ False when the reply left the template and the last decimal number is used."""
146
+ m = re.search(r"equals\s*(-?\d+\.?\d*)", text)
147
+ if m:
148
+ return float(m.group(1)), True
149
+ m = re.findall(r"-?\d+\.\d+", text)
150
+ return (float(m[-1]), False) if m else (None, False)
151
+
152
+
153
+ # ------------------------------------------------------------------------ bridges
154
+ def load_bridges(bridges_dir, d_model, n_layers, args):
155
+ """PsiBridgesMLX from <dir>/config.json['construct'] (defaults when the file is
156
+ absent, inferred from the tensors present) + <dir>/bridges.safetensors with
157
+ load_weights(strict=False): the Gemma export omits the retired learned-pointer
158
+ tensors (fwd.x0_query, fwd.x0_key.*), which the deterministic span pointer never
159
+ uses. Shapes of every provided tensor are checked explicitly, since non-strict
160
+ loading would not. Returns (bridges, config, coupling, report)."""
161
+ bridges_dir = Path(bridges_dir)
162
+ cfg_path = bridges_dir / "config.json"
163
+ cfg = json.loads(cfg_path.read_text()) if cfg_path.is_file() else {}
164
+ weights = mx.load(str(bridges_dir / "bridges.safetensors"))
165
+ keys = set(weights)
166
+
167
+ construct = {
168
+ "d_model": int(weights["fwd.query"].shape[0]) if "fwd.query" in keys else d_model,
169
+ "channel": "value" if any(k.startswith("val.") for k in keys) else "field",
170
+ "gate_bias": -2.0,
171
+ "inj_cap": None,
172
+ "readout_norm": "dim" if "fwd.dim_mu" in keys else "rms",
173
+ }
174
+ construct.update(cfg.get("construct", {}))
175
+ for name in ("channel", "gate_bias", "inj_cap", "readout_norm"): # CLI overrides
176
+ v = getattr(args, name)
177
+ if v is not None:
178
+ construct[name] = v
179
+ if int(construct["d_model"]) != int(d_model):
180
+ sys.exit(f"bridges were trained for hidden size {construct['d_model']} but the "
181
+ f"backbone has {d_model}: bridges do not transfer between backbones")
182
+ bridges = PsiBridgesMLX(**construct)
183
+
184
+ # explicit shape check + missing/unexpected report
185
+ params = dict(tree_flatten(bridges.parameters()))
186
+ missing = sorted(k for k in params if k not in keys)
187
+ unexpected = sorted(k for k in keys if k not in params)
188
+ bad = [(k, tuple(weights[k].shape), tuple(params[k].shape))
189
+ for k in keys if k in params and tuple(weights[k].shape) != tuple(params[k].shape)]
190
+ if bad:
191
+ lines = "\n".join(f" {k}: file {a} vs module {b}" for k, a, b in bad)
192
+ sys.exit(f"bridge tensor shapes do not match the constructed bridges:\n{lines}")
193
+ if unexpected:
194
+ sys.exit(f"bridges.safetensors has tensors the bridges do not define: {unexpected}")
195
+ bridges.load_weights(list(weights.items()), strict=False)
196
+ mx.eval(bridges.parameters())
197
+
198
+ coupling = dict(cfg.get("coupling", {}))
199
+ if args.l_fwd is not None:
200
+ coupling["l_fwd"] = args.l_fwd
201
+ if args.l_rev is not None:
202
+ coupling["l_rev"] = args.l_rev
203
+ if "n_layers" in coupling and int(coupling["n_layers"]) != int(n_layers):
204
+ sys.exit(f"config.json expects a {coupling['n_layers']}-layer backbone; "
205
+ f"this one has {n_layers}")
206
+ n_trained = sum(int(np.prod(v.shape)) for k, v in params.items() if k in keys)
207
+ report = {"construct": construct, "missing": missing, "n_tensors": len(keys),
208
+ "n_params": n_trained}
209
+ return bridges, cfg, coupling, report
210
+
211
+
212
+ def load_physics(path):
213
+ path = str(path)
214
+ if path.endswith(".pt"):
215
+ return convert_from_torch(path)
216
+ return load_fno_safetensors(path)
217
+
218
+
219
+ # --------------------------------------------------------------------------- main
220
+ def build_parser():
221
+ ap = argparse.ArgumentParser(
222
+ description="PsiLM (frozen LLM + frozen Burgers FNO through latent bridges): "
223
+ "answer one field-value question three ways.",
224
+ formatter_class=argparse.RawDescriptionHelpFormatter,
225
+ epilog="Training ranges: a in [0.5, 1.5], phi in [0, 6.28], x0 in [0, 0.99], "
226
+ "all with two decimals. Outside them the bridges are extrapolating.")
227
+ ap.add_argument("--a", type=float, default=1.28, help="amplitude of u(x,0) = a sin(2 pi x + phi)")
228
+ ap.add_argument("--phi", type=float, default=0.5, help="phase (radians)")
229
+ ap.add_argument("--x0", type=float, default=0.76, help="queried position in [0, 1)")
230
+ ap.add_argument("--backbone", default=DEFAULT_BACKBONE,
231
+ help="mlx-lm checkpoint (Hugging Face id or local path)")
232
+ ap.add_argument("--bridges", default=str(DEFAULT_BRIDGES),
233
+ help="directory with bridges.safetensors (+ config.json)")
234
+ ap.add_argument("--physics", default=str(DEFAULT_PHYSICS),
235
+ help="FNO weights: safetensors export, or a PyTorch fno.pt")
236
+ ap.add_argument("--hf-tokenizer", default=None,
237
+ help="HF tokenizer id for the chat template (default: config.json's "
238
+ "hf_tokenizer, else the backbone id)")
239
+ ap.add_argument("--no-baseline", action="store_true", help="skip the backbone-alone arm")
240
+ ap.add_argument("--question-only", action="store_true",
241
+ help="print the question the models see and exit (loads nothing)")
242
+ ap.add_argument("--max-new", type=int, default=24, help="PsiLM reply budget (tokens)")
243
+ ap.add_argument("--baseline-max-new", type=int, default=768,
244
+ help="backbone-alone reply budget before answer forcing (the "
245
+ "evaluation used 768; Gemma 4 uses all of it, ~75 s)")
246
+ g = ap.add_argument_group("bridge construction overrides (default: config.json)")
247
+ g.add_argument("--channel", choices=["value", "field"], default=None)
248
+ g.add_argument("--gate-bias", type=float, default=None)
249
+ g.add_argument("--inj-cap", type=float, default=None)
250
+ g.add_argument("--readout-norm", choices=["rms", "dim"], default=None)
251
+ g.add_argument("--l-fwd", type=int, default=None, help="readout layer")
252
+ g.add_argument("--l-rev", type=int, default=None, help="injection layer")
253
+ ap.add_argument("-v", "--verbose", action="store_true")
254
+ return ap
255
+
256
+
257
+ def _clean(text):
258
+ return re.sub(r"<\|?[a-z_|]+\|?>", "", text).strip()
259
+
260
+
261
+ def main():
262
+ args = build_parser().parse_args()
263
+ item = {"a": round(args.a, 2), "phi": round(args.phi, 2), "x0": round(args.x0, 2)}
264
+ if (item["a"], item["phi"], item["x0"]) != (args.a, args.phi, args.x0):
265
+ print(f"note: inputs rounded to two decimals (the trained readout reads 2-decimal "
266
+ f"numbers): a={item['a']} phi={item['phi']} x0={item['x0']}")
267
+ if not (0.0 <= item["x0"] < 1.0):
268
+ sys.exit("x0 must lie in [0, 1): the domain is periodic")
269
+ if not (0.5 <= item["a"] <= 1.5 and 0.0 <= item["phi"] <= 6.28):
270
+ print("warning: a or phi lies outside the training ranges (a in [0.5, 1.5], "
271
+ "phi in [0, 6.28]); the bridges are extrapolating")
272
+ question = QUESTION.format(a=item["a"], phi=item["phi"], x0=item["x0"])
273
+
274
+ if args.question_only:
275
+ print(f"[system] {SYSTEM}")
276
+ print(f"[user] {question}")
277
+ print(f"\n(the backbone-alone arm appends: {NUDGE.strip()!r})")
278
+ return
279
+
280
+ # ---- load
281
+ t0 = time.time()
282
+ print(f"backbone : {args.backbone}", flush=True)
283
+ model, stock, tok = load_backbone_any(args.backbone)
284
+ n_layers = len(model.model.layers)
285
+ d_model = int(model.args.hidden_size)
286
+ bridges, cfg, coupling, rep = load_bridges(args.bridges, d_model, n_layers, args)
287
+ hf_id = args.hf_tokenizer or cfg.get("hf_tokenizer") or args.backbone
288
+ hf_tok = AutoTokenizer.from_pretrained(hf_id)
289
+ fno = load_physics(args.physics)
290
+ psi = PsiLMMLX(model, tok, fno, bridges, l_fwd=coupling.get("l_fwd"), l_rev=coupling.get("l_rev"))
291
+ builder = QABuilder(hf_tok)
292
+ t_load = time.time() - t0
293
+ c = rep["construct"]
294
+ print(f"bridges : {args.bridges}\n"
295
+ f" channel={c['channel']} readout_norm={c['readout_norm']} "
296
+ f"gate_bias={c['gate_bias']} inj_cap={c['inj_cap']} | "
297
+ f"{rep['n_params']/1e6:.1f}M params in {rep['n_tensors']} tensors"
298
+ + (f" | not in file (unused): {rep['missing']}" if rep["missing"] else ""))
299
+ print(f"coupling : read @ layer {psi.l_fwd}, inject @ layer {psi.l_rev} of {psi.n_layers} "
300
+ f"(hidden {d_model}"
301
+ + (")" if cfg.get("coupling") else "; no config.json coupling entry: PsiLMMLX defaults)"))
302
+ # parameter count with each complex spectral weight counted once (wr/wi are one number)
303
+ n_fno = sum(int(np.prod(v.shape)) for k, v in tree_flatten(fno.parameters()) if not k.endswith(".wi"))
304
+ print(f"physics : {args.physics} (FNO1d, {n_fno/1e3:.0f}K params)")
305
+ print(f"loaded in {t_load:.1f} s\n")
306
+ print(f"question : {question}\n", flush=True)
307
+
308
+ # ---- 1. PsiLM: the coupled system
309
+ t1 = time.time()
310
+ psi_text = psi.generate(builder, item, max_new=args.max_new)
311
+ t_psi = time.time() - t1
312
+ psi_val, strict = parse_psilm_answer(psi_text)
313
+ print(f"[1] PsiLM (coupled) : {_clean(psi_text)!r}")
314
+ print(f" value : {psi_val if psi_val is not None else 'no number parsed'}"
315
+ f" ({t_psi:.1f} s{'' if strict or psi_val is None else '; reply left the trained template, last number taken'})",
316
+ flush=True)
317
+
318
+ # ---- 2. backbone alone
319
+ base_val = None
320
+ if not args.no_baseline:
321
+ gen, parse, nudge, src = _baseline_protocol()
322
+ t2 = time.time()
323
+ base_text, forced = gen(stock, hf_tok, question + nudge, args.baseline_max_new,
324
+ gen_tok=tok, force_answer=True)
325
+ t_base = time.time() - t2
326
+ base_val = parse(base_text)
327
+ tail = _clean(base_text)
328
+ tail = tail if args.verbose or len(tail) <= 240 else "..." + tail[-240:]
329
+ print(f"[2] backbone alone : {tail!r}")
330
+ print(f" value : {base_val if base_val is not None else 'no number parsed'}"
331
+ f" ({t_base:.1f} s, {len(tok.encode(base_text))} tokens"
332
+ f"{', answer forced' if forced else ''}; protocol: {src})", flush=True)
333
+ else:
334
+ print("[2] backbone alone : skipped (--no-baseline)")
335
+
336
+ # ---- 3. the physics model on the true initial condition
337
+ t3 = time.time()
338
+ params = mx.array([[item["a"], math.sin(item["phi"]), math.cos(item["phi"])]], dtype=mx.float32)
339
+ ic = build_ic_mlx(params) # the bridges' IC parameterization
340
+ u_field = np.array(fno(ic), dtype=np.float64)[0] # u(x, t=0.5) on the 128-grid
341
+ u_fno = fourier_interp(u_field, item["x0"])
342
+ t_fno = time.time() - t3
343
+ u_true = fourier_interp(solve(initial_condition(item["a"], item["phi"])), item["x0"])
344
+ if args.verbose:
345
+ ic_np = initial_condition(item["a"], item["phi"])
346
+ print(f" (IC parameterization check: max|build_ic - initial_condition| = "
347
+ f"{float(np.abs(np.array(ic)[0] - ic_np).max()):.2e})")
348
+ print(f"[3] physics model (FNO) : u({item['x0']}) = {u_fno:+.4f} ({t_fno*1e3:.0f} ms; "
349
+ f"the reference the coupled answer should match to +-{TOL})")
350
+ print(f" spectral solver : u({item['x0']}) = {u_true:+.4f} (ground truth)")
351
+
352
+ # ---- verdict
353
+ def mark(v):
354
+ return "n/a" if v is None else ("match" if abs(v - u_fno) <= TOL else "off")
355
+ if psi_val is None:
356
+ verdict = "PsiLM n/a (no number in the reply)"
357
+ else:
358
+ verdict = (f"PsiLM {mark(psi_val)} (|{psi_val}-{u_fno:.2f}| "
359
+ f"{'<=' if abs(psi_val-u_fno) <= TOL else '>'} {TOL})")
360
+ print("\n" + verdict + ("" if args.no_baseline else f"; backbone alone {mark(base_val)}"))
361
+
362
+
363
+ if __name__ == "__main__":
364
+ main()
requirements.txt ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PsiLM on Gemma 4 12B -- Apple Silicon (MLX). Verified 2026-09-06 on macOS / Python 3.11
2
+ # with: mlx 0.32.2, mlx-lm 0.31.3, transformers 5.16.1, torch 2.13.0, huggingface_hub 1.29.0.
3
+ #
4
+ # pip install -r requirements.txt
5
+ # python psilm_infer.py
6
+ #
7
+ mlx>=0.32
8
+ mlx-lm==0.31.3 # exact: the Gemma 4 loader (psilm.mlx.gemma_loader) goes through
9
+ # mlx_lm.utils internals; other versions are untested
10
+ transformers>=4.51 # chat template only (apply_chat_template); the model runs in MLX
11
+ torch>=2.4 # imported by psilm.stage2.qa (batching helpers); CPU wheel is fine
12
+ huggingface_hub>=0.30 # downloads the backbone from mlx-community
13
+ numpy>=1.26
14
+ safetensors>=0.4
15
+
16
+ # The PsiLM package itself (bridges, staged forward, Gemma loader, FNO, QA builder).
17
+ # Alternative to this line: clone the repository and set PSILM_REPO=/path/to/PsiLM.
18
+ psilm @ git+https://github.com/ryoji-info/PsiLM