julien-gadonneix commited on
Commit
199b932
·
verified ·
1 Parent(s): c4f7e0c

CORTEG pretrained model, converted from the official releases

Browse files
Files changed (7) hide show
  1. LICENSE +24 -0
  2. README.md +71 -0
  3. config.json +27 -0
  4. convert_corteg_checkpoint.py +532 -0
  5. model.safetensors +3 -0
  6. pytorch_model.bin +3 -0
  7. report.json +17 -0
LICENSE ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ The ST-EEGFormer small backbone (release ST-EEGFormer-small of
2
+ https://github.com/LiuyinYang1101/STEEGFormer):
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2025 LIUYIN YANG
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: braindecode
3
+ license: mit
4
+ pipeline_tag: feature-extraction
5
+ base_model: braindecode/STEEGFormer-small
6
+ tags:
7
+ - braindecode
8
+ - CORTEG
9
+ - ieeg
10
+ - ecog
11
+ - foundation-model
12
+ - pytorch_model_hub_mixin
13
+ ---
14
+
15
+ # CORTEG, ST-EEGFormer small backbone
16
+
17
+ The starting point of CORTEG ([Yang et al., 2026](https://arxiv.org/abs/2605.10337)),
18
+ ST-EEGFormer small adapted to intracranial recordings, for
19
+ [`braindecode.models.CORTEG`](https://braindecode.org/stable/generated/braindecode.models.CORTEG.html):
20
+ the ST-EEGFormer small encoder ([`checkpoint-300.pth`](https://github.com/LiuyinYang1101/STEEGFormer/releases/download/ST-EEGFormer-small/checkpoint-300.pth),
21
+ sha256 `98d2848abdaea7a143d399c07f67f8782e07dd66e7c309ddb058c7158a8a8b02`, through its conversion
22
+ [braindecode/STEEGFormer-small](https://huggingface.co/braindecode/STEEGFormer-small) at revision
23
+ `633c9c0d`), 25.3M parameters. The
24
+ high-gamma patch embedding, which the scalp model does not have, is warm-started
25
+ from the low-frequency one, linearly interpolated from 16 to 25 samples, as the
26
+ authors' `build_model` does ([LiuyinYang1101/CORTEG](https://github.com/LiuyinYang1101/CORTEG/tree/caafb78fbebdd9448814f96d7e61083a3eb62663),
27
+ `experiments/run_regression_hilo_clean.py`); it matches theirs to
28
+ 0.0e+00 in float32 on CPU. `convert_corteg_checkpoint.py` in this
29
+ repository reproduces the conversion.
30
+
31
+ ## Usage
32
+
33
+ ```python
34
+ from braindecode.models import CORTEG
35
+
36
+ model = CORTEG.from_pretrained(
37
+ "braindecode/corteg-small-pretrained", n_outputs=5, n_times=128, chs_info=raw.info["chs"]
38
+ )
39
+ ```
40
+
41
+ The spatial adapter, the head and, with `merge_strategy="layerwise_gate"`, the
42
+ gate are not in these files: Braindecode initializes them on load, the adapter
43
+ from the electrode positions of `chs_info` (metres). The paper then trains the
44
+ adapter, the high-gamma embedding, LoRA (r=4, alpha=16) on the last four blocks
45
+ with their LayerNorms, and the head, with the rest frozen. See
46
+ [`braindecode/corteg-stanford-pooled`](https://huggingface.co/braindecode/corteg-stanford-pooled)
47
+ for the authors' trained Stanford model and the input streams.
48
+
49
+ ## Limitations
50
+
51
+ These files hold no trained intracranial component, so the model needs
52
+ training before prediction. The check above covers the float32 CPU tensors of
53
+ the warm start.
54
+
55
+ ## Citation
56
+
57
+ ```bibtex
58
+ @inproceedings{yang2026corteg,
59
+ title = {{CORTEG}: Foundation Models Enable Cross-Modality Representation Transfer
60
+ from Scalp to Intracranial Brain Recordings},
61
+ author = {Yang, Liuyin and Sun, Qiang and Van Dyck, Bob and Calvo Merino, Eva and
62
+ Van Hulle, Marc M.},
63
+ booktitle = {Advances in Neural Information Processing Systems (NeurIPS)},
64
+ year = {2026}
65
+ }
66
+ ```
67
+
68
+ ## License
69
+
70
+ The ST-EEGFormer weights are released by Liuyin Yang under the MIT licence
71
+ (`LICENSE`). Braindecode's code is BSD-3-Clause.
config.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "n_outputs": 2,
3
+ "n_chans": 8,
4
+ "chs_info": null,
5
+ "n_times": 128,
6
+ "input_window_seconds": null,
7
+ "sfreq": 128.0,
8
+ "patch_size": 16,
9
+ "hi_patch_size": 25,
10
+ "embed_dim": 512,
11
+ "depth": 8,
12
+ "num_heads": 8,
13
+ "mlp_ratio": 4,
14
+ "drop_prob": 0.0,
15
+ "drop_path": 0.1,
16
+ "activation": "torch.nn.modules.activation.GELU",
17
+ "n_chans_pos": 145,
18
+ "merge_strategy": "average",
19
+ "n_merge_blocks": 4,
20
+ "knn_k": 8,
21
+ "adapter_hidden": 128,
22
+ "n_fourier_freqs": 32,
23
+ "fourier_max_freq": 50.0,
24
+ "gate_bottleneck": 16,
25
+ "on_non_divisible": "pad",
26
+ "braindecode_version": "2.1.0dev0"
27
+ }
convert_corteg_checkpoint.py ADDED
@@ -0,0 +1,532 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Authors: Julien Gadonneix <juliengado.2001@gmail.com>
2
+ # License: BSD (3-clause)
3
+ """Convert, check and publish the CORTEG checkpoints on CPU.
4
+
5
+ Run from an editable Braindecode checkout containing CORTEG, with the ``hub``
6
+ extra and ``timm`` and ``einops`` (for the reference) installed::
7
+
8
+ git clone https://github.com/LiuyinYang1101/CORTEG && git -C CORTEG checkout caafb78
9
+ curl -L -o checkpoint-300.pth \\
10
+ https://github.com/LiuyinYang1101/STEEGFormer/releases/download/ST-EEGFormer-small/checkpoint-300.pth
11
+ python convert_corteg_checkpoint.py --reference-dir CORTEG \\
12
+ --backbone checkpoint-300.pth --output-dir /tmp/corteg-converted
13
+
14
+ ``--reference-dir`` is a clone of the official repository at ``REVISION``, which
15
+ ships the Stanford adapter (``ADAPTER``); ``--backbone`` is the ST-EEGFormer
16
+ small release CORTEG starts from. The SHA-256 values below must match. The port
17
+ takes the backbone from its Braindecode conversion (``BACKBONE_REPO`` at
18
+ ``BACKBONE_REVISION``). Two models are written (``--variant`` picks one):
19
+
20
+ - ``pretrained``: the backbone, with the high-gamma patch embedding warm-started
21
+ from the low-frequency one by linear interpolation, as the authors'
22
+ ``build_model`` does. The spatial adapter, gate and head are left out:
23
+ Braindecode initializes them on load, the adapter from the electrode
24
+ positions. The warm start must match the reference's within ``TOLERANCE``.
25
+ - ``stanford``: that model with the released adapter, which holds the trainable
26
+ parameters of the authors' Stanford run (the spatial adapter, LoRA factors on
27
+ blocks 4-7, the high-gamma patch embedding, those blocks' LayerNorms and the
28
+ readout). The LoRA factors are folded into the fused qkv, proj, fc1 and fc2
29
+ weights, so the model is a plain ``CORTEG``. The authors' ``load_corteg``
30
+ builds the reference from the two release files; the outputs must agree
31
+ within ``TOLERANCE``.
32
+
33
+ Download this script from either Braindecode CORTEG model repository on Hugging
34
+ Face; it is not bundled in the Braindecode source tree. Add ``--push-to
35
+ braindecode`` to upload each converted model, this script, the MIT licences, a
36
+ report and a model card in one commit.
37
+ """
38
+
39
+ import argparse
40
+ import hashlib
41
+ import json
42
+ import shutil
43
+ import sys
44
+ import tempfile
45
+ from contextlib import contextmanager
46
+ from pathlib import Path
47
+ from types import SimpleNamespace
48
+
49
+ import numpy as np
50
+ import torch
51
+ from huggingface_hub import HfApi, hf_hub_download
52
+ from safetensors.torch import load_file
53
+ from torch import nn
54
+ from torch.nn import functional as F
55
+
56
+ from braindecode.models import CORTEG
57
+
58
+ REPOSITORY = "https://github.com/LiuyinYang1101/CORTEG"
59
+ REVISION = "caafb78fbebdd9448814f96d7e61083a3eb62663"
60
+ ADAPTER = "checkpoints/corteg_stanford_pooled.pt"
61
+ ADAPTER_SHA256 = "77d63a214e0e0f2c790e90878a0ef6b525f6e24e1453fcd94e118c63ff7e8396"
62
+ ADAPTER_PARAMS = 297_236 # the release manifest's trainable_params
63
+ BACKBONE_URL = (
64
+ "https://github.com/LiuyinYang1101/STEEGFormer/releases/download/"
65
+ "ST-EEGFormer-small/checkpoint-300.pth"
66
+ )
67
+ BACKBONE_SHA256 = "98d2848abdaea7a143d399c07f67f8782e07dd66e7c309ddb058c7158a8a8b02"
68
+ BACKBONE_REPO = "braindecode/STEEGFormer-small"
69
+ BACKBONE_REVISION = "633c9c0d76c6e26459e1e0d1337c91858578f13d"
70
+ REPOS = {"pretrained": "corteg-small-pretrained", "stanford": "corteg-stanford-pooled"}
71
+ # The paper's setting (the Stanford manifest's architecture_args): 1 s windows of
72
+ # 128 low-frequency and 200 high-gamma samples, the "average" fusion before the
73
+ # last four blocks. The Stanford head has one output per finger.
74
+ CONFIG = dict(
75
+ n_times=128,
76
+ sfreq=128.0,
77
+ merge_strategy="average",
78
+ n_merge_blocks=4,
79
+ hi_patch_size=25,
80
+ knn_k=8,
81
+ drop_path=0.1,
82
+ )
83
+ N_OUTPUTS = {"pretrained": 2, "stanford": 5}
84
+ # Left out of the pretrained files: initialized on load (the adapter from chs_info).
85
+ NOT_PRETRAINED = ("channel_adapter", "final_layer")
86
+ LORA_SCALE = 16 / 4 # alpha / r
87
+ # Reference layer of a block -> its Braindecode modules; the fused qkv splits in three.
88
+ LAYERS = {
89
+ "norm1": ["0.fn.0"],
90
+ "norm2": ["1.fn.0"],
91
+ "attn.qkv": ["0.fn.1.queries", "0.fn.1.keys", "0.fn.1.values"],
92
+ "attn.proj": ["0.fn.1.projection"],
93
+ "mlp.fc1": ["1.fn.1.0"],
94
+ "mlp.fc2": ["1.fn.1.3"],
95
+ }
96
+ TOLERANCE = 1e-4
97
+ FINGERS = ["thumb", "index", "middle", "ring", "pinky"]
98
+ MIT = """MIT License
99
+
100
+ Copyright (c) {year} {holder}
101
+
102
+ Permission is hereby granted, free of charge, to any person obtaining a copy
103
+ of this software and associated documentation files (the "Software"), to deal
104
+ in the Software without restriction, including without limitation the rights
105
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
106
+ copies of the Software, and to permit persons to whom the Software is
107
+ furnished to do so, subject to the following conditions:
108
+
109
+ The above copyright notice and this permission notice shall be included in all
110
+ copies or substantial portions of the Software.
111
+
112
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
113
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
114
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
115
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
116
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
117
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
118
+ SOFTWARE.
119
+ """
120
+ STEEGFORMER_LICENSE = (
121
+ "The ST-EEGFormer small backbone (release ST-EEGFormer-small of\n"
122
+ "https://github.com/LiuyinYang1101/STEEGFormer):\n\n"
123
+ + MIT.format(year=2025, holder="LIUYIN YANG")
124
+ )
125
+ CORTEG_LICENSE = (
126
+ f"The CORTEG adapter ({ADAPTER} of\n{REPOSITORY} at {REVISION}):\n\n"
127
+ + MIT.format(year=2026, holder="Liuyin Yang")
128
+ )
129
+ LICENSES = {
130
+ "pretrained": STEEGFORMER_LICENSE,
131
+ "stanford": CORTEG_LICENSE + "\n\n" + STEEGFORMER_LICENSE,
132
+ }
133
+ CITATION = """```bibtex
134
+ @inproceedings{yang2026corteg,
135
+ title = {{CORTEG}: Foundation Models Enable Cross-Modality Representation Transfer
136
+ from Scalp to Intracranial Brain Recordings},
137
+ author = {Yang, Liuyin and Sun, Qiang and Van Dyck, Bob and Calvo Merino, Eva and
138
+ Van Hulle, Marc M.},
139
+ booktitle = {Advances in Neural Information Processing Systems (NeurIPS)},
140
+ year = {2026}
141
+ }
142
+ ```"""
143
+
144
+
145
+ def sha256(path):
146
+ digest = hashlib.sha256()
147
+ with open(path, "rb") as f:
148
+ for block in iter(lambda: f.read(1 << 20), b""):
149
+ digest.update(block)
150
+ return digest.hexdigest()
151
+
152
+
153
+ def montages():
154
+ """Electrode positions in metres: the Braindecode test montage and 46 random contacts."""
155
+ rng = np.random.default_rng(0)
156
+ random = rng.uniform([-0.07, -0.10, -0.04], [0.07, 0.07, 0.08], size=(46, 3))
157
+ return [
158
+ torch.tensor(
159
+ [[-0.04 + 0.004 * i, -0.02 + 0.003 * i, 0.06 - 0.002 * i] for i in range(8)]
160
+ ),
161
+ torch.tensor(random, dtype=torch.float32),
162
+ ]
163
+
164
+
165
+ def load_backbone(model):
166
+ """ST-EEGFormer small into ``model``, the high-gamma embedding warm-started.
167
+
168
+ Returns the keys the backbone leaves to the adapter.
169
+ """
170
+ path = hf_hub_download(BACKBONE_REPO, "model.safetensors", revision=BACKBONE_REVISION)
171
+ backbone = {k: v for k, v in load_file(path).items() if not k.startswith("final_layer.")}
172
+ missing, unexpected = model.load_state_dict(backbone, strict=False)
173
+ expected = {"channel_adapter", "final_layer", "patch_embed_hi"}
174
+ if unexpected or {k.split(".")[0] for k in missing} != expected:
175
+ raise SystemExit(f"backbone does not fit CORTEG: {missing=}, {unexpected=}")
176
+ lo, hi = model.patch_embed.proj, model.patch_embed_hi.proj
177
+ with torch.no_grad():
178
+ hi.weight.copy_(F.interpolate(lo.weight[None], size=hi.in_features, mode="linear")[0])
179
+ hi.bias.copy_(lo.bias)
180
+ return missing
181
+
182
+
183
+ def fold_adapter(adapter, model, missing):
184
+ """Load the released adapter into ``model``, the LoRA factors folded in."""
185
+ state = model.state_dict()
186
+ filled = set()
187
+ for key, value in adapter.items():
188
+ key = key.removeprefix("backbone.").replace("head.head.", "final_layer.")
189
+ if not key.startswith("blocks."):
190
+ if key not in state or state[key].shape != value.shape:
191
+ raise SystemExit(f"adapter tensor {key} has no home in CORTEG")
192
+ state[key] = value
193
+ filled.add(key)
194
+ continue
195
+ _, block, rest = key.split(".", 2)
196
+ layer, _, param = rest.rpartition(".")
197
+ if layer.endswith(".B"): # folded with its A factor
198
+ continue
199
+ if layer.endswith(".A"):
200
+ layer = layer.removesuffix(".A")
201
+ lora_b = adapter[f"backbone.blocks.{block}.{layer}.B.weight"]
202
+ delta = (LORA_SCALE * lora_b @ value).chunk(len(LAYERS[layer]))
203
+ for name, part in zip(LAYERS[layer], delta):
204
+ state[f"encoder.{block}.{name}.weight"] += part
205
+ else:
206
+ target = f"encoder.{block}.{LAYERS[layer][0]}.{param}"
207
+ state[target] = value
208
+ filled.add(target)
209
+ unfilled = {k for k in missing if k not in filled}
210
+ if unfilled:
211
+ raise SystemExit(f"the adapter leaves {sorted(unfilled)} at their initialization")
212
+ model.load_state_dict(state, strict=True)
213
+
214
+
215
+ def build(variant, adapter, n_chans):
216
+ model = CORTEG(n_chans=n_chans, n_outputs=N_OUTPUTS[variant], **CONFIG).eval()
217
+ missing = load_backbone(model)
218
+ if variant == "stanford":
219
+ fold_adapter(adapter, model, missing)
220
+ return model
221
+
222
+
223
+ @contextmanager
224
+ def reference(reference_dir, backbone):
225
+ """The authors' code, with a backbone configuration pointing at ``backbone``."""
226
+ sys.path.insert(0, str(reference_dir))
227
+ config = json.loads((reference_dir / "configs/steegformer_small.json").read_text())
228
+ config["pretrained"]["path"] = str(backbone.resolve())
229
+ with tempfile.TemporaryDirectory() as tmp:
230
+ path = Path(tmp) / "steegformer_small.json"
231
+ path.write_text(json.dumps(config))
232
+ yield path
233
+
234
+
235
+ def check_pretrained(model, reference_dir, backbone, pos):
236
+ """Largest difference between the warm-started high-gamma embeddings."""
237
+ manifest = json.loads((reference_dir / ADAPTER).with_suffix(".json").read_text())
238
+ with reference(reference_dir, backbone) as config:
239
+ from experiments.run_regression_hilo_clean import build_model
240
+
241
+ overrides = dict(model_kwargs_json=str(config), no_pretrained=False)
242
+ args = SimpleNamespace(**(manifest["build_args"] | overrides))
243
+ ref = build_model(args, C_in=len(pos), T_in=128, ecog_xyz_m=1000 * pos.numpy())
244
+ state = ref.state_dict()
245
+ proj = model.patch_embed_hi.proj
246
+ return max(
247
+ (proj.weight - state["backbone.patch_embed_hi.proj.weight"]).abs().max().item(),
248
+ (proj.bias - state["backbone.patch_embed_hi.proj.bias"]).abs().max().item(),
249
+ )
250
+
251
+
252
+ def check_stanford(model, reference_dir, backbone, pos):
253
+ """Largest output difference with the authors' ``load_corteg``."""
254
+ n_chans = len(pos)
255
+ g = torch.Generator().manual_seed(0)
256
+ x = torch.randn(4, n_chans, 128, generator=g)
257
+ x_hi = torch.randn(4, n_chans, 200, generator=g)
258
+ with torch.no_grad():
259
+ out = model(x, x_hi, pos=pos)
260
+ with reference(reference_dir, backbone) as config:
261
+ from load_corteg import load_corteg, predict
262
+
263
+ ref = load_corteg(
264
+ C_in=n_chans,
265
+ T_in=128,
266
+ ecog_xyz_mm=1000 * pos.numpy(),
267
+ d_out=N_OUTPUTS["stanford"],
268
+ backbone_config=str(config),
269
+ )
270
+ expected = torch.from_numpy(predict(ref, x.numpy(), x_hi.numpy()))
271
+ return (out - expected).abs().max().item()
272
+
273
+
274
+ @contextmanager
275
+ def without(model, names):
276
+ """Hide the ``names`` submodules so that their tensors are not serialized."""
277
+ hidden = {name: getattr(model, name) for name in names}
278
+ for name in names:
279
+ setattr(model, name, nn.Identity())
280
+ try:
281
+ yield
282
+ finally:
283
+ for name, module in hidden.items():
284
+ setattr(model, name, module)
285
+
286
+
287
+ def pretrained_card(report):
288
+ return f"""---
289
+ library_name: braindecode
290
+ license: mit
291
+ pipeline_tag: feature-extraction
292
+ base_model: {BACKBONE_REPO}
293
+ tags:
294
+ - braindecode
295
+ - CORTEG
296
+ - ieeg
297
+ - ecog
298
+ - foundation-model
299
+ - pytorch_model_hub_mixin
300
+ ---
301
+
302
+ # CORTEG, ST-EEGFormer small backbone
303
+
304
+ The starting point of CORTEG ([Yang et al., 2026](https://arxiv.org/abs/2605.10337)),
305
+ ST-EEGFormer small adapted to intracranial recordings, for
306
+ [`braindecode.models.CORTEG`](https://braindecode.org/stable/generated/braindecode.models.CORTEG.html):
307
+ the ST-EEGFormer small encoder ([`checkpoint-300.pth`]({BACKBONE_URL}),
308
+ sha256 `{BACKBONE_SHA256}`, through its conversion
309
+ [{BACKBONE_REPO}](https://huggingface.co/{BACKBONE_REPO}) at revision
310
+ `{BACKBONE_REVISION[:8]}`), {report["n_params"] / 1e6:.1f}M parameters. The
311
+ high-gamma patch embedding, which the scalp model does not have, is warm-started
312
+ from the low-frequency one, linearly interpolated from 16 to 25 samples, as the
313
+ authors' `build_model` does ([LiuyinYang1101/CORTEG]({REPOSITORY}/tree/{REVISION}),
314
+ `experiments/run_regression_hilo_clean.py`); it matches theirs to
315
+ {report["max_abs_diff"]:.1e} in float32 on CPU. `convert_corteg_checkpoint.py` in this
316
+ repository reproduces the conversion.
317
+
318
+ ## Usage
319
+
320
+ ```python
321
+ from braindecode.models import CORTEG
322
+
323
+ model = CORTEG.from_pretrained(
324
+ "braindecode/{REPOS["pretrained"]}", n_outputs=5, n_times=128, chs_info=raw.info["chs"]
325
+ )
326
+ ```
327
+
328
+ The spatial adapter, the head and, with `merge_strategy="layerwise_gate"`, the
329
+ gate are not in these files: Braindecode initializes them on load, the adapter
330
+ from the electrode positions of `chs_info` (metres). The paper then trains the
331
+ adapter, the high-gamma embedding, LoRA (r=4, alpha=16) on the last four blocks
332
+ with their LayerNorms, and the head, with the rest frozen. See
333
+ [`braindecode/{REPOS["stanford"]}`](https://huggingface.co/braindecode/{REPOS["stanford"]})
334
+ for the authors' trained Stanford model and the input streams.
335
+
336
+ ## Limitations
337
+
338
+ These files hold no trained intracranial component, so the model needs
339
+ training before prediction. The check above covers the float32 CPU tensors of
340
+ the warm start.
341
+
342
+ ## Citation
343
+
344
+ {CITATION}
345
+
346
+ ## License
347
+
348
+ The ST-EEGFormer weights are released by Liuyin Yang under the MIT licence
349
+ (`LICENSE`). Braindecode's code is BSD-3-Clause.
350
+ """
351
+
352
+
353
+ def stanford_card(report):
354
+ return f"""---
355
+ library_name: braindecode
356
+ license: mit
357
+ base_model: braindecode/{REPOS["pretrained"]}
358
+ tags:
359
+ - braindecode
360
+ - CORTEG
361
+ - ieeg
362
+ - ecog
363
+ - finger-flexion
364
+ - pytorch_model_hub_mixin
365
+ ---
366
+
367
+ # CORTEG, Stanford finger flexion (pooled)
368
+
369
+ The released CORTEG model of [Yang et al. (2026)](https://arxiv.org/abs/2605.10337),
370
+ ST-EEGFormer small adapted to ECoG and trained on the finger-flexion recordings
371
+ of the 9 Stanford patients pooled (Table 1, "CORTEG (pooled)", Pearson r 0.554),
372
+ {report["n_params"] / 1e6:.1f}M parameters, converted to
373
+ [`braindecode.models.CORTEG`](https://braindecode.org/stable/generated/braindecode.models.CORTEG.html).
374
+ It decodes the flexion of the five fingers ({", ".join(FINGERS)}) from 1 s of ECoG.
375
+
376
+ Sources:
377
+
378
+ - the adapter, `{ADAPTER}` of [LiuyinYang1101/CORTEG]({REPOSITORY}/tree/{REVISION}),
379
+ sha256 `{ADAPTER_SHA256}`: the spatial adapter, LoRA factors (r=4, alpha=16) on
380
+ blocks 4-7, the high-gamma patch embedding, those blocks' LayerNorms and the
381
+ readout, {ADAPTER_PARAMS:,} parameters;
382
+ - the frozen backbone it was trained on, the ST-EEGFormer small release
383
+ ([`checkpoint-300.pth`]({BACKBONE_URL}), sha256 `{BACKBONE_SHA256}`), here
384
+ through [braindecode/{REPOS["pretrained"]}](https://huggingface.co/braindecode/{REPOS["pretrained"]}).
385
+
386
+ Conversion folds the LoRA factors into the fused qkv, proj, fc1 and fc2 weights
387
+ (scale alpha/r = 4), so the model is a plain `CORTEG`;
388
+ `convert_corteg_checkpoint.py` in this repository reproduces it. Outputs match
389
+ the authors' `load_corteg` on the two release files to {report["max_abs_diff"]:.1e}
390
+ in float32 on CPU, for montages of {" and ".join(str(n) for n in report["n_chans_checked"])}
391
+ contacts.
392
+
393
+ ## Usage
394
+
395
+ The model reads two streams of the same 1 s window, as the paper's Stanford
396
+ pipeline (`data/stanford_preprocessing/` of the official repository): after a
397
+ notch at 60, 120 and 180 Hz and a common average reference, `x` is the signal
398
+ resampled to 128 Hz and `x_hi` its 70-200 Hz Hilbert envelope at 200 Hz, each
399
+ z-scored per channel with the statistics of the training windows. Electrode
400
+ positions are in metres, in `chs_info` or `pos`.
401
+
402
+ ```python
403
+ import torch
404
+ from braindecode.models import CORTEG
405
+
406
+ model = CORTEG.from_pretrained(
407
+ "braindecode/{REPOS["stanford"]}", chs_info=raw.info["chs"]
408
+ ).eval()
409
+ flexion = model(x, x_hi) # x: (batch, n_chans, 128), x_hi: (batch, n_chans, 200)
410
+ ```
411
+
412
+ The output, `(batch, 5)`, is the flexion of each finger, z-scored per finger
413
+ with the training statistics of the patient, at the time 40 ms before the
414
+ window's end. The saved geometry is {report["n_chans"]} channels; the model
415
+ reads any montage, so pass your own `chs_info` (or `n_chans` and `pos` at each
416
+ call) when loading.
417
+
418
+ ## Limitations
419
+
420
+ The readout is at its random initialization, as released: the paper run built
421
+ it after the optimizer, so it was never trained, and the adapter learned around
422
+ it (see the release manifest, `checkpoints/corteg_stanford_pooled.json`).
423
+ The model was trained on the training split of the 9 Stanford patients (the
424
+ first 400 s of each recording, two thirds of one under 600 s); other patients
425
+ or tasks need fine-tuning, as in the paper's leave-one-subject-out protocol.
426
+ The check above covers float32 CPU outputs, not decoding accuracy, GPU kernels
427
+ or mixed precision; the authors report that this checkpoint reproduces the
428
+ cohort mean of 0.554 (manifest, `paper.verified`).
429
+
430
+ ## Citation
431
+
432
+ {CITATION}
433
+
434
+ ## License
435
+
436
+ The adapter and the ST-EEGFormer backbone are both released by Liuyin Yang
437
+ under the MIT licence (`LICENSE`). Braindecode's code is BSD-3-Clause.
438
+ """
439
+
440
+
441
+ CARDS = {"pretrained": pretrained_card, "stanford": stanford_card}
442
+
443
+
444
+ def convert(variant, adapter, args):
445
+ """Convert, check and write one model; push it if asked."""
446
+ check = check_pretrained if variant == "pretrained" else check_stanford
447
+ max_abs_diff, n_chans_checked = 0.0, []
448
+ for pos in montages():
449
+ model = build(variant, adapter, len(pos))
450
+ diff = check(model, args.reference_dir, args.backbone, pos)
451
+ max_abs_diff = max(max_abs_diff, diff)
452
+ n_chans_checked.append(len(pos))
453
+ if max_abs_diff > TOLERANCE:
454
+ raise SystemExit(f"{variant}: differs by {max_abs_diff:.2e} > {TOLERANCE}")
455
+
456
+ # Users pass their own montage when loading; the weights do not depend on it.
457
+ n_chans = n_chans_checked[0]
458
+ model = build(variant, adapter, n_chans)
459
+ hidden = NOT_PRETRAINED if variant == "pretrained" else ()
460
+ with without(model, hidden):
461
+ n_params = sum(t.numel() for t in model.state_dict().values())
462
+ report = {
463
+ "backbone": f"{BACKBONE_URL} via {BACKBONE_REPO}@{BACKBONE_REVISION}",
464
+ "backbone_sha256": BACKBONE_SHA256,
465
+ "n_params": n_params,
466
+ "max_abs_diff": max_abs_diff,
467
+ "n_chans_checked": n_chans_checked,
468
+ "n_chans": n_chans,
469
+ "n_times": CONFIG["n_times"],
470
+ "torch_version": torch.__version__,
471
+ }
472
+ if variant == "pretrained":
473
+ report["not_saved"] = list(NOT_PRETRAINED)
474
+ else:
475
+ report |= {
476
+ "source": f"{REPOSITORY}@{REVISION}:{ADAPTER}",
477
+ "sha256": ADAPTER_SHA256,
478
+ "adapter_tensors": len(adapter),
479
+ "adapter_params": ADAPTER_PARAMS,
480
+ "lora_scale": LORA_SCALE,
481
+ }
482
+ print(variant, json.dumps(report, indent=2))
483
+ destination = args.output_dir / REPOS[variant]
484
+ destination.mkdir(parents=True, exist_ok=True)
485
+ with without(model, hidden):
486
+ model.save_pretrained(destination)
487
+ # The Hub rejects NaN and Infinity, which Python's json writes by default.
488
+ json.loads(
489
+ (destination / "config.json").read_text(),
490
+ parse_constant=lambda c: (_ for _ in ()).throw(ValueError(f"{c} in config.json")),
491
+ )
492
+ shutil.copyfile(Path(__file__).resolve(), destination / Path(__file__).name)
493
+ (destination / "LICENSE").write_text(LICENSES[variant])
494
+ (destination / "report.json").write_text(json.dumps(report, indent=2))
495
+ # Written last: save_pretrained leaves a placeholder card behind.
496
+ (destination / "README.md").write_text(CARDS[variant](report))
497
+ if args.push_to:
498
+ repo_id = f"{args.push_to}/{REPOS[variant]}"
499
+ api = HfApi()
500
+ api.create_repo(repo_id, exist_ok=True)
501
+ api.upload_folder(
502
+ folder_path=str(destination),
503
+ repo_id=repo_id,
504
+ commit_message=f"CORTEG {variant} model, converted from the official releases",
505
+ )
506
+ print(f"published https://huggingface.co/{repo_id}")
507
+
508
+
509
+ def main():
510
+ parser = argparse.ArgumentParser(description=__doc__)
511
+ parser.add_argument("--reference-dir", type=Path, required=True)
512
+ parser.add_argument("--backbone", type=Path, required=True)
513
+ parser.add_argument("--output-dir", type=Path, required=True)
514
+ parser.add_argument("--variant", choices=sorted(REPOS), action="append")
515
+ parser.add_argument("--push-to", help="Hub namespace to publish the models to")
516
+ args = parser.parse_args()
517
+ torch.set_num_threads(1)
518
+ adapter_path = args.reference_dir / ADAPTER
519
+ for path, expected in ((adapter_path, ADAPTER_SHA256), (args.backbone, BACKBONE_SHA256)):
520
+ if sha256(path) != expected:
521
+ raise SystemExit(f"{path} is not the released file (sha256 mismatch)")
522
+ adapter = torch.load(adapter_path, map_location="cpu", weights_only=True)
523
+ n_adapter = sum(t.numel() for t in adapter.values())
524
+ if n_adapter != ADAPTER_PARAMS:
525
+ raise SystemExit(f"adapter holds {n_adapter} parameters, not {ADAPTER_PARAMS}")
526
+ for variant in args.variant or REPOS:
527
+ torch.manual_seed(0)
528
+ convert(variant, adapter, args)
529
+
530
+
531
+ if __name__ == "__main__":
532
+ main()
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:558c367ac471d2f233917a500c360e60fc883f72f8dfb2836a30906fe1e095b8
3
+ size 101280432
pytorch_model.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6a0f6b342fc36ba1eec9bba052dc967508d2b28f2819c1af0a4051407258ef38
3
+ size 101318275
report.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "backbone": "https://github.com/LiuyinYang1101/STEEGFormer/releases/download/ST-EEGFormer-small/checkpoint-300.pth via braindecode/STEEGFormer-small@633c9c0d76c6e26459e1e0d1337c91858578f13d",
3
+ "backbone_sha256": "98d2848abdaea7a143d399c07f67f8782e07dd66e7c309ddb058c7158a8a8b02",
4
+ "n_params": 25316864,
5
+ "max_abs_diff": 0.0,
6
+ "n_chans_checked": [
7
+ 8,
8
+ 46
9
+ ],
10
+ "n_chans": 8,
11
+ "n_times": 128,
12
+ "torch_version": "2.15.0+cu132",
13
+ "not_saved": [
14
+ "channel_adapter",
15
+ "final_layer"
16
+ ]
17
+ }