1use super::{Framework, ModelSpec, ModelStage, NetworkMemory, WeightFile, WeightStatus};
16
17const fn mem(
22 resident_bytes: u64,
23 load_peak_bytes: u64,
24 inference_fixed_bytes: u64,
25 inference_bytes_per_voxel: u64,
26 wasm_fixed_bytes: u64,
27 wasm_bytes_per_voxel: u64,
28) -> NetworkMemory {
29 NetworkMemory {
30 resident_bytes,
31 load_peak_bytes,
32 inference_fixed_bytes,
33 inference_bytes_per_voxel,
34 wasm_fixed_bytes,
35 wasm_bytes_per_voxel,
36 }
37}
38
39pub fn all_models() -> &'static [ModelSpec] {
41 MODELS
42}
43
44pub fn find_model(id: &str) -> Option<&'static ModelSpec> {
46 MODELS.iter().find(|m| m.id == id)
47}
48
49const fn pending_onnx(name: &'static str) -> WeightFile {
51 WeightFile { name, url: "", sha256: "", bytes: 0 }
52}
53
54const MODELS: &[ModelSpec] = &[
55 ModelSpec {
57 id: "bfrnet",
58 name: "BFRnet",
59 stage: ModelStage::BackgroundRemoval,
60 status: WeightStatus::Available,
61 origin: Framework::Matlab,
62 description: "Dual-frequency octave-convolution U-Net for background field \
63 removal (total field → local field). Fully convolutional.",
64 paper: "Zhu et al., Z Med Phys 2023; doi:10.1016/j.zemedi.2022.08.001",
65 source: "https://github.com/sunhongfu/BFRnet",
66 license: "",
67 files: &[WeightFile {
70 name: "bfrnet.onnx",
71 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/bfrnet.onnx",
72 sha256: "6f693f0a02c94550179c4b5188ce652fc4bd8198ff57aaddd102fba67fe873d7",
73 bytes: 79_600_612,
74 }],
75 inputs: &["field"],
76 outputs: &["local_field"],
77 size_divisor: 8,
78 tile_bytes_per_voxel: Some(2_054),
80 memory: mem(82_100_000, 164_200_000, 128_000_000, 1738, 245_000_000, 2556),
81 },
82 ModelSpec {
84 id: "xqsm",
85 name: "xQSM",
86 stage: ModelStage::DipoleInversion,
87 status: WeightStatus::Available,
88 origin: Framework::PyTorch,
89 description: "Octave-convolution U-Net with a learned residual for dipole \
90 inversion (local field → susceptibility).",
91 paper: "Gao et al., NMR Biomed 2021; doi:10.1002/nbm.4461",
92 source: "https://github.com/sunhongfu/xQSM",
93 license: "",
94 files: &[WeightFile {
97 name: "xqsm.onnx",
98 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/xqsm.onnx",
99 sha256: "81854ec2ca85bba25c2f9aae05efdcac299d0efffb03fe6a0e6797019e46b487",
100 bytes: 20_901_826,
101 }],
102 inputs: &["field"],
103 outputs: &["chi"],
104 size_divisor: 8,
105 tile_bytes_per_voxel: Some(1_146),
107 memory: mem(21_600_000, 43_800_000, 59_000_000, 913, 243_000_000, 980),
108 },
109 ModelSpec {
110 id: "qsmnet",
111 name: "QSMnet",
112 stage: ModelStage::DipoleInversion,
113 status: WeightStatus::Available,
114 origin: Framework::TensorFlow,
115 description: "3D U-Net dipole inversion trained on COSMOS. Expects 1 mm \
116 isotropic input; z-scored with training mean/std.",
117 paper: "Yoon et al., NeuroImage 2018; doi:10.1016/j.neuroimage.2018.06.030",
118 source: "https://github.com/SNU-LIST/QSMnet",
119 license: "",
120 files: &[WeightFile {
124 name: "qsmnet.onnx",
125 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/qsmnet.onnx",
126 sha256: "f1b625e3c04e679077cb8ac9b2617b14e4bbe452f9ae516a806a1acd36e2b159",
127 bytes: 397_801_168,
128 }],
129 inputs: &["field"],
130 outputs: &["chi"],
131 size_divisor: 16,
132 tile_bytes_per_voxel: Some(1_694),
135 memory: mem(398_000_000, 796_200_000, 443_000_000, 842, 1_008_000_000, 1068),
136 },
137 ModelSpec {
138 id: "qsmnet-plus",
139 name: "QSMnet+",
140 stage: ModelStage::DipoleInversion,
141 status: WeightStatus::Available,
142 origin: Framework::TensorFlow,
143 description: "QSMnet retrained with susceptibility-scaling augmentation for \
144 a wider, more linear χ range.",
145 paper: "Jung et al., NeuroImage 2020; doi:10.1016/j.neuroimage.2020.116579",
146 source: "https://github.com/SNU-LIST/QSMnet",
147 license: "",
148 files: &[WeightFile {
151 name: "qsmnet-plus.onnx",
152 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/qsmnet-plus.onnx",
153 sha256: "0ed31f9a1b66f75fee4a96022b6bc3838b91bc5b819714dade5ca87cd331683d",
154 bytes: 397_801_854,
155 }],
156 inputs: &["field"],
157 outputs: &["chi"],
158 size_divisor: 16,
159 tile_bytes_per_voxel: Some(1_694),
162 memory: mem(398_000_000, 796_200_000, 443_000_000, 842, 1_008_000_000, 1068),
163 },
164 ModelSpec {
165 id: "qsmnetme",
166 name: "QSMnetME",
167 stage: ModelStage::DipoleInversion,
168 status: WeightStatus::Available,
169 origin: Framework::Onnx,
170 description: "The SNU-LIST χ-separation toolbox's QSM network (already ONNX): a \
171 QSMnet-style 3D U-Net trained on multi-echo data, run by the toolbox's \
172 route (field rotated to B0 along z and resampled to 1 mm, z-scored with \
173 the χ-sepnet field statistics, whole volume, mapped back). Spatial axes \
174 re-declared dynamic (the toolbox ships a fixed 192×192×128 input). \
175 Normalization constants are baked into the Rust glue (QsmnetmeNorm).",
176 paper: "Kim et al., Hum Brain Mapp 2025 (doi:10.1002/hbm.70136)",
177 source: "https://github.com/SNU-LIST/chi-separation",
178 license: "",
179 files: &[WeightFile {
184 name: "qsmnetme.onnx",
185 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/qsmnetme.onnx",
186 sha256: "c2f030b625b2fc83f3ae45b38c996e006a069444d2da32d2b6022045c92c01e5",
187 bytes: 90_307_128,
188 }],
189 inputs: &["field"],
190 outputs: &["chi"],
191 size_divisor: 16,
192 tile_bytes_per_voxel: None,
193 memory: mem(90_500_000, 181_200_000, 114_000_000, 825, 188_000_000, 1155),
196 },
197 ModelSpec {
198 id: "qsmgan",
199 name: "QSMGAN",
200 stage: ModelStage::DipoleInversion,
201 status: WeightStatus::Available,
202 origin: Framework::PyTorch,
203 description: "3D U-Net generator (WGAN-GP refined) for dipole inversion, run \
204 patch-wise (64³ input → 48³ output, i64o48). Only the generator is \
205 used at inference; the sign flip, input_scale/tanh (χ=atanh/10) and \
206 patch tiling live in the Rust glue.",
207 paper: "Chen et al., NeuroImage 2020; doi:10.1016/j.neuroimage.2019.116389",
208 source: "https://github.com/mmorri10/QSMGAN-LupoLab",
209 license: "MIT (fork)",
210 files: &[WeightFile {
211 name: "qsmgan.onnx",
212 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/qsmgan.onnx",
213 sha256: "238573a6fa8a563004fc28e63a30694289d1a1a204f3c406daba486b1122d604",
214 bytes: 10_753_955,
215 }],
216 inputs: &["field"],
217 outputs: &["chi"],
218 size_divisor: 8,
219 tile_bytes_per_voxel: None,
220 memory: mem(11_000_000, 22_200_000, 0, 518, 0, 699),
221 },
222 ModelSpec {
223 id: "lpcnn",
224 name: "LPCNN",
225 stage: ModelStage::DipoleInversion,
226 status: WeightStatus::Available,
227 origin: Framework::PyTorch,
228 description: "Learned proximal CNN, 3 unrolled iterations of proximal \
229 gradient descent. The k-space dipole data-consistency step, the \
230 unroll, the learned step size and the mean/std normalization run \
231 in Rust (rustfft); only the learned proximal CNN is ONNX.",
232 paper: "Lai et al., MICCAI 2020; doi:10.1007/978-3-030-59713-9_13",
233 source: "https://github.com/Sulam-Group/LPCNN",
234 license: "",
235 files: &[WeightFile {
236 name: "lpcnn.onnx",
237 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/lpcnn.onnx",
238 sha256: "831f2a161f6ab8ee7bf64cc2dd8bca532fc971d0083cf5729d6302d3ed31625d",
239 bytes: 1_790_869,
240 }],
241 inputs: &["field", "mask", "b_vec"],
242 outputs: &["chi"],
243 size_divisor: 8,
244 tile_bytes_per_voxel: Some(964),
247 memory: mem(2_000_000, 4_100_000, 11_000_000, 661, 0, 766),
248 },
249 ModelSpec {
250 id: "ir2qsm",
251 name: "IR2QSM",
252 stage: ModelStage::DipoleInversion,
253 status: WeightStatus::Available,
254 origin: Framework::PyTorch,
255 description: "IR2U-net dipole inversion: a 3D U-net (depth 4) run for 4 \
256 unrolled iterations with reverse concatenations and a recurrent \
257 middle module. The whole net is ONNX; the Rust glue only does \
258 /8 zero-pad, crop and mask (no normalization, ppm in/out). The \
259 ungated inference-time AddNoise is pinned to its noise-free \
260 branch for deterministic export.",
261 paper: "Li et al., Med. Phys. 2025; doi:10.1002/mp.17747; arXiv:2406.12300",
262 source: "https://github.com/YangGaoUQ/IR2QSM",
263 license: "",
264 files: &[WeightFile {
265 name: "ir2qsm.onnx",
266 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/ir2qsm.onnx",
267 sha256: "dcaf7a26d6633900f339f94be16d580ccea6147bbf6da26eacb9a5e1acdc0c53",
268 bytes: 40_259_354,
269 }],
270 inputs: &["field", "mask"],
271 outputs: &["chi"],
272 size_divisor: 8,
273 tile_bytes_per_voxel: Some(904),
275 memory: mem(41_000_000, 82_900_000, 74_000_000, 704, 147_000_000, 769),
276 },
277 ModelSpec {
279 id: "autoqsm",
280 name: "AutoQSM",
281 stage: ModelStage::SingleStep,
282 status: WeightStatus::Available,
283 origin: Framework::TensorFlow,
284 description: "V-Net single-step reconstruction (total field → susceptibility) \
285 with no separate brain extraction. Fixed 64³→32³ patch net; \
286 the Rust glue does the sliding-window tiling + blend.",
287 paper: "Wei et al., NeuroImage 2019; doi:10.1016/j.neuroimage.2019.116064",
288 source: "https://github.com/AMRI-Lab/AutoQSM",
289 license: "",
290 files: &[WeightFile {
293 name: "autoqsm.onnx",
294 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/autoqsm.onnx",
295 sha256: "c2f23c6ae735fd1677e8623da22cb6f25b4a932ae50ba1ef08a75439e240977e",
296 bytes: 5_609_662,
297 }],
298 inputs: &["field"],
301 outputs: &["chi"],
302 size_divisor: 1,
303 tile_bytes_per_voxel: None,
304 memory: mem(5_800_000, 11_800_000, 0, 495, 0, 638),
305 },
306 ModelSpec {
307 id: "iqsm",
308 name: "iQSM",
309 stage: ModelStage::SingleStep,
310 status: WeightStatus::Available,
311 origin: Framework::PyTorch,
312 description: "LoT-Unet single-step reconstruction from wrapped phase to \
313 susceptibility. The learnable-Laplacian front-end is fused into \
314 the exported graph; inputs are phase, mask, TE (s), B0 (T).",
315 paper: "Gao et al., NeuroImage 2022; doi:10.1016/j.neuroimage.2022.119410",
316 source: "https://github.com/sunhongfu/iQSM",
317 license: "",
318 files: &[WeightFile {
319 name: "iqsm.onnx",
320 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/iqsm.onnx",
321 sha256: "8538a33892a877812e6cd0e22927a5040827c126e9347073d3dd0934bf226b09",
322 bytes: 17_233_763,
323 }],
324 inputs: &["phase", "mask", "te", "b0", "border"],
325 outputs: &["chi"],
326 size_divisor: 16,
327 tile_bytes_per_voxel: Some(600),
329 memory: mem(17_600_000, 35_300_000, 34_000_000, 476, 55_000_000, 571),
330 },
331 ModelSpec {
332 id: "iqsm-plus",
333 name: "iQSM+",
334 stage: ModelStage::SingleStep,
335 status: WeightStatus::Available,
336 origin: Framework::PyTorch,
337 description: "iQSM with orientation-adaptive latent feature editing (OA-LFE); \
338 the B0 direction is a genuine network input. Inputs: phase, \
339 mask, TE, B0, z_prjs (B0 dir), border.",
340 paper: "Gao et al., Med Image Anal 2024; doi:10.1016/j.media.2024.103160",
341 source: "https://github.com/sunhongfu/iQSM_Plus",
342 license: "",
343 files: &[WeightFile {
344 name: "iqsm-plus.onnx",
345 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/iqsm-plus.onnx",
346 sha256: "f93a4c83804759a4a0d24603f7754efb997ddd2ba5e3a0346ed6493725cafb1d",
347 bytes: 17_740_526,
348 }],
349 inputs: &["phase", "mask", "te", "b0", "z_prjs", "border"],
350 outputs: &["chi"],
351 size_divisor: 16,
352 tile_bytes_per_voxel: Some(1_149),
354 memory: mem(20_300_000, 41_000_000, 31_000_000, 1055, 65_000_000, 1037),
355 },
356 ModelSpec {
357 id: "iqfm",
358 name: "iQFM",
359 stage: ModelStage::PhaseToField,
360 status: WeightStatus::Available,
361 origin: Framework::PyTorch,
362 description: "The tissue-field head of the iQSM LoT-Unet: wrapped phase → \
363 local (background-removed) field in one network (joint unwrap + \
364 BFR). Same architecture/inputs as iQSM, `lfs` weights; output is \
365 the local field (ppm), not susceptibility.",
366 paper: "Gao et al., NeuroImage 2022; doi:10.1016/j.neuroimage.2022.119410",
367 source: "https://github.com/sunhongfu/iQSM",
368 license: "",
369 files: &[WeightFile {
370 name: "iqfm.onnx",
371 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/iqfm.onnx",
372 sha256: "81965f4c0981612d979977c9618847e4cb1d340efa287b8dde894bdea5bf6ac9",
373 bytes: 17_233_763,
374 }],
375 inputs: &["phase", "mask", "te", "b0", "border"],
376 outputs: &["localfield"],
377 size_divisor: 16,
378 tile_bytes_per_voxel: Some(600),
380 memory: mem(17_600_000, 35_300_000, 34_000_000, 476, 55_000_000, 571),
381 },
382 ModelSpec {
383 id: "nextqsm",
384 name: "NeXtQSM",
385 stage: ModelStage::SingleStep,
386 status: WeightStatus::Available,
387 origin: Framework::TensorFlow,
388 description: "U-Net background removal followed by a 6-step variational-network \
389 dipole inversion (total field → susceptibility). Reimplemented as \
390 a Rust hybrid: two exported U-Nets (`nextqsm-bf` = BFR forward, \
391 `nextqsm-vjp` = the regularizer gradient ∇ₓ mean|VarNet(x)| written \
392 as a forward graph) plus the FFT data-consistency gradient and unroll \
393 in Rust. Order the files BFR-first, VJP-second.",
394 paper: "Cognolato et al., Medical Image Analysis 84:102700 (2023); doi:10.1016/j.media.2022.102700",
395 source: "https://github.com/QSMxT/NeXtQSM",
396 license: "MIT",
397 files: &[
398 WeightFile {
399 name: "nextqsm-bf.onnx",
400 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/nextqsm-bf.onnx",
401 sha256: "d0f0b0391153f8e5e07ef85d46fbe492afc8394fab008350540a6868d0b4a3ba",
402 bytes: 113_211_474,
403 },
404 WeightFile {
405 name: "nextqsm-vjp.onnx",
406 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/nextqsm-vjp.onnx",
407 sha256: "50fe388663c05b1d3996139975270e500e13feb025f1b792576dbc0da814a4bc",
408 bytes: 42_437_407,
409 },
410 ],
411 inputs: &["field", "mask", "b_vec"],
412 outputs: &["chi"],
413 size_divisor: 64,
414 tile_bytes_per_voxel: Some(1_952),
417 memory: mem(156_200_000, 226_900_000, 110_000_000, 600, 316_000_000, 753),
418 },
419 ModelSpec {
420 id: "modl-qsm",
421 name: "MoDL-QSM",
422 stage: ModelStage::DipoleInversion,
423 status: WeightStatus::Available,
424 origin: Framework::TensorFlow,
425 description: "Model-based deep learning: 3-iteration unroll of a QSM \
426 forward-model gradient descent with a learned 2-channel CNN \
427 prior. The k-space dipole data-consistency (A/Aᴴ), the unroll, \
428 the learned step size and per-channel mean/std normalization run \
429 in Rust (rustfft); only the CNN prior is ONNX. Output is the STI \
430 χ33 component.",
431 paper: "Feng et al., NeuroImage 2021; doi:10.1016/j.neuroimage.2021.118376",
432 source: "https://github.com/Ruimin-Feng/MoDL-QSM",
433 license: "",
434 files: &[WeightFile {
435 name: "modl-qsm.onnx",
436 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/modl-qsm.onnx",
437 sha256: "1f8da7d39eaaa4c3f0f9e67298fba5f14230e769cfd6349c81d0b74a6f800c57",
438 bytes: 1_794_143,
439 }],
440 inputs: &["field", "mask", "b_vec"],
441 outputs: &["chi"],
442 size_divisor: 1,
443 tile_bytes_per_voxel: Some(909),
446 memory: mem(2_000_000, 4_100_000, 11_000_000, 665, 12_000_000, 629),
447 },
448 ModelSpec {
450 id: "susep-net",
451 name: "SUSEP-Net",
452 stage: ModelStage::ChiSeparation,
453 status: WeightStatus::Available,
454 origin: Framework::PyTorch,
455 description: "Dual-branch 3D U-Net source separation from z-scored \
456 [QSM, R2', local field] → [χ+, χ−]. Normalization constants \
457 are baked into the Rust glue (SusepNetNorm).",
458 paper: "Li, Gao, Sun et al., arXiv:2506.13293 (2025)",
459 source: "https://github.com/YangGaoUQ/SUSEP-Net",
460 license: "",
461 files: &[WeightFile {
462 name: "susep-net.onnx",
463 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/susep-net.onnx",
464 sha256: "9bc85aa28451fda2c661de4b8fae8604ced7a54f028f94ed33629f6111a48b03",
465 bytes: 205_743_584,
466 }],
467 inputs: &["qsm", "r2prime", "lfs"],
468 outputs: &["chi_pos", "chi_neg"],
469 size_divisor: 8,
470 tile_bytes_per_voxel: None,
471 memory: mem(206_100_000, 412_700_000, 249_000_000, 1960, 553_000_000, 2164),
472 },
473 ModelSpec {
474 id: "r2primenet",
475 name: "R2PRIMEnet",
476 stage: ModelStage::R2PrimeGeneration,
477 status: WeightStatus::Available,
478 origin: Framework::Onnx,
479 description: "SNU-LIST R2*→R2′ conversion network from the χ-sepnet pipeline \
480 (already ONNX): a fully-convolutional 3D U-Net mapping the Dr-scaled, \
481 z-scored R2* map to R2′, run as an overlapping sliding window. Supplies \
482 R2′ for the GRE-only condition, where no spin-echo R2 is measured. \
483 Spatial axes re-declared dynamic (the authors ship a fixed 192×192×128 \
484 input) so 32-bit hosts can use a smaller patch. Normalization constants \
485 (Dr=114) are baked into the Rust glue (R2PrimeNetNorm).",
486 paper: "Kim et al., Hum Brain Mapp 2025 (doi:10.1002/hbm.70136)",
487 source: "https://github.com/SNU-LIST/chi_sepnet",
488 license: "",
489 files: &[WeightFile {
490 name: "r2primenet.onnx",
491 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/r2primenet.onnx",
492 sha256: "44cb5e67d1c68dae87a5f532e501dc0d4bf627d91f9c8ba35a56df5ac7ec3cd0",
493 bytes: 90_307_128,
494 }],
495 inputs: &["r2star"],
496 outputs: &["r2prime"],
497 size_divisor: 16,
498 tile_bytes_per_voxel: None,
499 memory: mem(90_500_000, 181_200_000, 114_000_000, 825, 188_000_000, 1155),
500 },
501 ModelSpec {
502 id: "chi-sepnet",
503 name: "χ-sepnet",
504 stage: ModelStage::ChiSeparation,
505 status: WeightStatus::Available,
506 origin: Framework::Onnx,
507 description: "SNU-LIST χ-separation network (already ONNX): a fully-convolutional \
508 3D U-Net mapping [QSM, local field, R2′/Dr] (z-scored) → [χ+, χ−], \
509 run by the toolbox's route (training frame, QSMnetME QSM channel, whole \
510 volume) or alone as an overlapping sliding window. Spatial axes re-declared dynamic \
511 (the authors ship a fixed 192×192×128 input) so 32-bit hosts can use a \
512 smaller patch. Normalization constants (Dr=114) are baked into the Rust \
513 glue (ChiSepNetNorm).",
514 paper: "Kim et al. / SNU-LIST chi-separation toolbox",
515 source: "https://github.com/SNU-LIST/chi-separation",
516 license: "",
517 files: &[WeightFile {
518 name: "chi-sepnet.onnx",
519 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/chi-sepnet.onnx",
520 sha256: "5b442fdfdb88f50ec9149b384dabd0d2d9adb6983947d9cfa58382b78676bed9",
521 bytes: 90_314_172,
522 }],
523 inputs: &["local_field", "qsm", "r2prime"],
524 outputs: &["chi_pos", "chi_neg"],
525 size_divisor: 8,
526 tile_bytes_per_voxel: None,
527 memory: mem(90_500_000, 181_200_000, 114_000_000, 833, 188_000_000, 1155),
528 },
529 ModelSpec {
530 id: "hd-bet",
531 name: "HD-BET",
532 stage: ModelStage::BrainExtraction,
533 status: WeightStatus::Available,
534 origin: Framework::PyTorch,
535 description: "nnU-Net v2 3D U-Net brain extraction (HD-BET v2): magnitude → brain \
536 mask, trained on 11,751 multi-sequence clinical MRIs. Runs at 1 mm \
537 on the whole volume (or Gaussian-weighted sliding-window patches, \
538 96×192×192 as trained); nnU-Net's crop/normalise/resample pipeline \
539 is in the Rust glue (bet::hd_bet). Spatial axes are dynamic \
540 (multiples of 16×32×32).",
541 paper: "Isensee et al., Hum Brain Mapp 40(17):4952-4964 (2019); https://doi.org/10.1002/hbm.24750",
542 source: "https://github.com/MIC-DKFZ/HD-BET",
543 license: "CC-BY-NC-4.0",
544 files: &[WeightFile {
545 name: "hd-bet.onnx",
546 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/hd-bet.onnx",
547 sha256: "f15200f9a0697cf53b151e316a0c4fe2cacfdbe2220deb36cb56184be4a227f9",
548 bytes: 123_168_028,
549 }],
550 inputs: &["image"],
551 outputs: &["logits"],
552 size_divisor: 32,
553 tile_bytes_per_voxel: None,
554 memory: mem(123_500_000, 247_200_000, 162_000_000, 821, 307_000_000, 1101),
555 },
556 ModelSpec {
557 id: "rs2-net",
558 name: "RS2-Net",
559 stage: ModelStage::BrainExtraction,
560 status: WeightStatus::Available,
561 origin: Framework::PyTorch,
562 description: "Swin-UNETR rodent (mouse, rat) brain extraction, trained within nnU-Net \
563 v2 on 1,142 MRIs from 89 centres: magnitude → brain mask. Runs at \
564 0.25×0.2×0.16 mm with Gaussian-weighted sliding-window patches; nnU-Net's \
565 transpose/crop/normalise/resample pipeline is in the Rust glue \
566 (bet::rs2_net). The graph is traced at a fixed 128×96×128 patch.",
567 paper: "Lin et al., NeuroImage 298:120769 (2024); https://doi.org/10.1016/j.neuroimage.2024.120769",
568 source: "https://github.com/VitoLin21/Rodent-Skull-Stripping",
569 license: "GPL-3.0",
570 files: &[WeightFile {
574 name: "rs2-net.onnx",
575 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/rs2-net.onnx",
576 sha256: "a120ce43b06a9f3ddecf85bf18f281cbb54c354ef15b34a6bcaaffa4e3c4a570",
577 bytes: 63_456_162,
578 }],
579 inputs: &["input"],
580 outputs: &["logits"],
581 size_divisor: 32,
582 tile_bytes_per_voxel: None,
583 memory: mem(83_100_000, 159_600_000, 0, 1692, 0, 1697),
584 },
585 ModelSpec {
586 id: "mindgrab",
587 name: "MindGrab",
588 stage: ModelStage::BrainExtraction,
589 status: WeightStatus::Available,
590 origin: Framework::PyTorch,
591 description: "MeshNet brain extraction for any contrast (25 dilated 3×3×3 convolutions, \
592 15 channels, 146 k parameters) trained only on synthetic data: image → \
593 brain mask. Runs on the whole 256³ 1 mm LIA conformed volume; the \
594 conform/normalise/largest-component/reslice pipeline is in the Rust glue \
595 (bet::mindgrab). Spatial axes are dynamic.",
596 paper: "Fani et al., arXiv:2506.11860 (2025); https://doi.org/10.48550/arXiv.2506.11860",
597 source: "https://github.com/neuroneural/brainchop-cli",
598 license: "MIT",
599 files: &[WeightFile {
603 name: "mindgrab.onnx",
604 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/mindgrab.onnx",
605 sha256: "2e20e5e03211976d4c30b4e704c57f7f18cb1b78e840105acf12765c6e3c342b",
606 bytes: 672_150,
607 }],
608 inputs: &["image"],
609 outputs: &["logits"],
610 size_divisor: 1,
611 tile_bytes_per_voxel: None,
612 memory: mem(2_000_000, 3_600_000, 206_000_000, 426, 363_000_000, 311),
613 },
614 ModelSpec {
616 id: "synthseg",
617 name: "SynthSeg 1.0",
618 stage: ModelStage::Segmentation,
619 status: WeightStatus::Available,
620 origin: Framework::TensorFlow,
621 description: "Contrast-agnostic whole-brain segmentation: magnitude → 32 FreeSurfer \
622 labels. A 5-level 3D U-Net (24 features, ELU, 13.2 M parameters) trained \
623 only on synthetic images with randomised contrast, so it runs directly on \
624 GRE magnitude without a T1w scan. Resample/orient/normalise, flip \
625 averaging and the topological cleanup are in the Rust glue \
626 (segment::synthseg). Spatial axes are dynamic (multiples of 32).",
627 paper: "Billot et al., Med Image Anal 86:102789 (2023); https://doi.org/10.1016/j.media.2023.102789",
628 source: "https://github.com/BBillot/SynthSeg",
629 license: "Apache-2.0",
630 files: &[WeightFile {
633 name: "synthseg.onnx",
634 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/synthseg.onnx",
635 sha256: "c2821a74e8a03d4073896b5c2e359b3ef86f9776bacd78e00da1c757aa97bbcc",
636 bytes: 52_998_326,
637 }],
638 inputs: &["image"],
639 outputs: &["unet_prediction"],
640 size_divisor: 32,
641 tile_bytes_per_voxel: None,
642 memory: mem(53_400_000, 106_800_000, 87_000_000, 919, 132_000_000, 1187),
643 },
644 ModelSpec {
645 id: "synthseg-2.0",
646 name: "SynthSeg 2.0",
647 stage: ModelStage::Segmentation,
648 status: WeightStatus::Pending,
649 origin: Framework::TensorFlow,
650 description: "SynthSeg 2.0: same architecture as `synthseg` with a 33-label set (adds \
651 a general CSF class). The upstream weights are not in the SynthSeg \
652 repository — they ship with FreeSurfer, or come from the UCL download \
653 linked in the SynthSeg README — so they have to be fetched before \
654 exporting. Run it with SynthSegVersion::V2.",
655 paper: "Billot et al., PNAS 120(9):e2216399120 (2023); https://doi.org/10.1073/pnas.2216399120",
656 source: "https://github.com/BBillot/SynthSeg",
657 license: "Apache-2.0",
658 files: &[pending_onnx("synthseg-2.0.onnx")],
659 inputs: &["image"],
660 outputs: &["unet_prediction"],
661 size_divisor: 32,
662 tile_bytes_per_voxel: None,
663 memory: mem(53_400_000, 106_800_000, 87_000_000, 931, 132_000_000, 1203),
666 },
667];
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672
673 #[test]
674 fn ids_are_unique_and_findable() {
675 let mut seen = std::collections::HashSet::new();
676 for m in MODELS {
677 assert!(seen.insert(m.id), "duplicate model id: {}", m.id);
678 assert!(find_model(m.id).is_some(), "find_model failed for {}", m.id);
679 assert!(!m.name.is_empty(), "{} has empty name", m.id);
680 assert!(!m.files.is_empty(), "{} has no weight files", m.id);
681 }
682 assert!(find_model("does-not-exist").is_none());
683 }
684
685 #[test]
686 fn pending_models_are_not_available() {
687 for m in MODELS.iter().filter(|m| m.status == WeightStatus::Pending) {
690 assert!(!m.is_available(), "{} is Pending but reports available", m.id);
691 assert!(
692 m.files.iter().any(|f| f.url.is_empty()),
693 "{} is Pending but every file has a url — flip status to Available",
694 m.id
695 );
696 for f in m.files.iter().filter(|f| !f.sha256.is_empty()) {
698 assert_eq!(f.sha256.len(), 64, "{}/{} sha256 must be 64 hex chars", m.id, f.name);
699 assert!(
700 f.sha256.bytes().all(|b| b.is_ascii_hexdigit()),
701 "{}/{} sha256 not hex",
702 m.id,
703 f.name
704 );
705 }
706 }
707 }
708
709 #[test]
710 fn available_models_are_fully_specified() {
711 for m in MODELS.iter().filter(|m| m.status == WeightStatus::Available) {
713 for f in m.files {
714 assert!(!f.url.is_empty(), "{}/{} available but no url", m.id, f.name);
715 assert_eq!(f.sha256.len(), 64, "{}/{} sha256 must be 64 hex chars", m.id, f.name);
716 assert!(
717 f.sha256.bytes().all(|b| b.is_ascii_hexdigit()),
718 "{}/{} sha256 not hex",
719 m.id,
720 f.name
721 );
722 }
723 assert!(m.is_available());
724 }
725 }
726}