1use super::{Framework, ModelSpec, ModelStage, WeightFile, WeightStatus};
16
17pub fn all_models() -> &'static [ModelSpec] {
19 MODELS
20}
21
22pub fn find_model(id: &str) -> Option<&'static ModelSpec> {
24 MODELS.iter().find(|m| m.id == id)
25}
26
27const fn pending_onnx(name: &'static str) -> WeightFile {
29 WeightFile { name, url: "", sha256: "", bytes: 0 }
30}
31
32const MODELS: &[ModelSpec] = &[
33 ModelSpec {
35 id: "bfrnet",
36 name: "BFRnet",
37 stage: ModelStage::BackgroundRemoval,
38 status: WeightStatus::Available,
39 origin: Framework::Matlab,
40 description: "Dual-frequency octave-convolution U-Net for background field \
41 removal (total field → local field). Fully convolutional.",
42 paper: "Kames et al. / Sun group; https://github.com/sunhongfu/BFRnet",
43 source: "https://github.com/sunhongfu/BFRnet",
44 license: "",
45 files: &[WeightFile {
48 name: "bfrnet.onnx",
49 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/bfrnet.onnx",
50 sha256: "6f693f0a02c94550179c4b5188ce652fc4bd8198ff57aaddd102fba67fe873d7",
51 bytes: 79_600_612,
52 }],
53 inputs: &["field"],
54 outputs: &["local_field"],
55 size_divisor: 8,
56 },
57 ModelSpec {
59 id: "xqsm",
60 name: "xQSM",
61 stage: ModelStage::DipoleInversion,
62 status: WeightStatus::Available,
63 origin: Framework::PyTorch,
64 description: "Octave-convolution U-Net with a learned residual for dipole \
65 inversion (local field → susceptibility).",
66 paper: "Gao et al., NMR Biomed 2021; doi:10.1002/nbm.4461",
67 source: "https://github.com/sunhongfu/xQSM",
68 license: "",
69 files: &[WeightFile {
72 name: "xqsm.onnx",
73 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/xqsm.onnx",
74 sha256: "81854ec2ca85bba25c2f9aae05efdcac299d0efffb03fe6a0e6797019e46b487",
75 bytes: 20_901_826,
76 }],
77 inputs: &["field"],
78 outputs: &["chi"],
79 size_divisor: 8,
80 },
81 ModelSpec {
82 id: "qsmnet",
83 name: "QSMnet",
84 stage: ModelStage::DipoleInversion,
85 status: WeightStatus::Available,
86 origin: Framework::TensorFlow,
87 description: "3D U-Net dipole inversion trained on COSMOS. Expects 1 mm \
88 isotropic input; z-scored with training mean/std.",
89 paper: "Yoon et al., NeuroImage 2018; doi:10.1016/j.neuroimage.2018.06.030",
90 source: "https://github.com/SNU-LIST/QSMnet",
91 license: "",
92 files: &[WeightFile {
95 name: "qsmnet.onnx",
96 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/qsmnet.onnx",
97 sha256: "8fd1d79b7a9258a262a2faab9e8469757ae7735f3da6212e431acfb207c21b54",
98 bytes: 397_801_854,
99 }],
100 inputs: &["field"],
101 outputs: &["chi"],
102 size_divisor: 16,
103 },
104 ModelSpec {
105 id: "qsmnet-plus",
106 name: "QSMnet+",
107 stage: ModelStage::DipoleInversion,
108 status: WeightStatus::Available,
109 origin: Framework::TensorFlow,
110 description: "QSMnet retrained with susceptibility-scaling augmentation for \
111 a wider, more linear χ range.",
112 paper: "Jung et al., NeuroImage 2020; doi:10.1016/j.neuroimage.2020.116579",
113 source: "https://github.com/SNU-LIST/QSMnet",
114 license: "",
115 files: &[WeightFile {
118 name: "qsmnet-plus.onnx",
119 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/qsmnet-plus.onnx",
120 sha256: "0ed31f9a1b66f75fee4a96022b6bc3838b91bc5b819714dade5ca87cd331683d",
121 bytes: 397_801_854,
122 }],
123 inputs: &["field"],
124 outputs: &["chi"],
125 size_divisor: 16,
126 },
127 ModelSpec {
128 id: "qsmgan",
129 name: "QSMGAN",
130 stage: ModelStage::DipoleInversion,
131 status: WeightStatus::Available,
132 origin: Framework::PyTorch,
133 description: "3D U-Net generator (WGAN-GP refined) for dipole inversion, run \
134 patch-wise (64³ input → 48³ output, i64o48). Only the generator is \
135 used at inference; the sign flip, input_scale/tanh (χ=atanh/10) and \
136 patch tiling live in the Rust glue.",
137 paper: "Chen et al., NeuroImage 2020; doi:10.1016/j.neuroimage.2019.116389",
138 source: "https://github.com/mmorri10/QSMGAN-LupoLab",
139 license: "MIT (fork)",
140 files: &[WeightFile {
141 name: "qsmgan.onnx",
142 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/qsmgan.onnx",
143 sha256: "238573a6fa8a563004fc28e63a30694289d1a1a204f3c406daba486b1122d604",
144 bytes: 10_753_955,
145 }],
146 inputs: &["field"],
147 outputs: &["chi"],
148 size_divisor: 8,
149 },
150 ModelSpec {
151 id: "lpcnn",
152 name: "LPCNN",
153 stage: ModelStage::DipoleInversion,
154 status: WeightStatus::Available,
155 origin: Framework::PyTorch,
156 description: "Learned proximal CNN, 3 unrolled iterations of proximal \
157 gradient descent. The k-space dipole data-consistency step, the \
158 unroll, the learned step size and the mean/std normalization run \
159 in Rust (rustfft); only the learned proximal CNN is ONNX.",
160 paper: "Lai et al., MICCAI 2020; doi:10.1007/978-3-030-59713-9_13",
161 source: "https://github.com/Sulam-Group/LPCNN",
162 license: "",
163 files: &[WeightFile {
164 name: "lpcnn.onnx",
165 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/lpcnn.onnx",
166 sha256: "831f2a161f6ab8ee7bf64cc2dd8bca532fc971d0083cf5729d6302d3ed31625d",
167 bytes: 1_790_869,
168 }],
169 inputs: &["field", "mask", "b_vec"],
170 outputs: &["chi"],
171 size_divisor: 8,
172 },
173 ModelSpec {
174 id: "ir2qsm",
175 name: "IR2QSM",
176 stage: ModelStage::DipoleInversion,
177 status: WeightStatus::Available,
178 origin: Framework::PyTorch,
179 description: "IR2U-net dipole inversion: a 3D U-net (depth 4) run for 4 \
180 unrolled iterations with reverse concatenations and a recurrent \
181 middle module. The whole net is ONNX; the Rust glue only does \
182 /8 zero-pad, crop and mask (no normalization, ppm in/out). The \
183 ungated inference-time AddNoise is pinned to its noise-free \
184 branch for deterministic export.",
185 paper: "Li et al., Med. Phys. 2025; doi:10.1002/mp.17747; arXiv:2406.12300",
186 source: "https://github.com/YangGaoUQ/IR2QSM",
187 license: "",
188 files: &[WeightFile {
189 name: "ir2qsm.onnx",
190 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/ir2qsm.onnx",
191 sha256: "dcaf7a26d6633900f339f94be16d580ccea6147bbf6da26eacb9a5e1acdc0c53",
192 bytes: 40_259_354,
193 }],
194 inputs: &["field", "mask"],
195 outputs: &["chi"],
196 size_divisor: 8,
197 },
198 ModelSpec {
200 id: "autoqsm",
201 name: "AutoQSM",
202 stage: ModelStage::SingleStep,
203 status: WeightStatus::Available,
204 origin: Framework::TensorFlow,
205 description: "V-Net single-step reconstruction (total field → susceptibility) \
206 with no separate brain extraction. Fixed 64³→32³ patch net; \
207 the Rust glue does the sliding-window tiling + blend.",
208 paper: "Wei et al., NeuroImage 2019; doi:10.1016/j.neuroimage.2019.116064",
209 source: "https://github.com/AMRI-Lab/AutoQSM",
210 license: "",
211 files: &[WeightFile {
214 name: "autoqsm.onnx",
215 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/autoqsm.onnx",
216 sha256: "c2f23c6ae735fd1677e8623da22cb6f25b4a932ae50ba1ef08a75439e240977e",
217 bytes: 5_609_662,
218 }],
219 inputs: &["field"],
222 outputs: &["chi"],
223 size_divisor: 1,
224 },
225 ModelSpec {
226 id: "iqsm",
227 name: "iQSM",
228 stage: ModelStage::SingleStep,
229 status: WeightStatus::Available,
230 origin: Framework::PyTorch,
231 description: "LoT-Unet single-step reconstruction from wrapped phase to \
232 susceptibility. The learnable-Laplacian front-end is fused into \
233 the exported graph; inputs are phase, mask, TE (s), B0 (T).",
234 paper: "Gao et al., NeuroImage 2022; doi:10.1016/j.neuroimage.2022.119410",
235 source: "https://github.com/sunhongfu/iQSM",
236 license: "",
237 files: &[WeightFile {
238 name: "iqsm.onnx",
239 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/iqsm.onnx",
240 sha256: "8538a33892a877812e6cd0e22927a5040827c126e9347073d3dd0934bf226b09",
241 bytes: 17_233_763,
242 }],
243 inputs: &["phase", "mask", "te", "b0", "border"],
244 outputs: &["chi"],
245 size_divisor: 8,
246 },
247 ModelSpec {
248 id: "iqsm-plus",
249 name: "iQSM+",
250 stage: ModelStage::SingleStep,
251 status: WeightStatus::Available,
252 origin: Framework::PyTorch,
253 description: "iQSM with orientation-adaptive latent feature editing (OA-LFE); \
254 the B0 direction is a genuine network input. Inputs: phase, \
255 mask, TE, B0, z_prjs (B0 dir), border.",
256 paper: "Gao et al., Med Image Anal 2024; doi:10.1016/j.media.2024.103160",
257 source: "https://github.com/sunhongfu/iQSM_Plus",
258 license: "",
259 files: &[WeightFile {
260 name: "iqsm-plus.onnx",
261 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/iqsm-plus.onnx",
262 sha256: "f93a4c83804759a4a0d24603f7754efb997ddd2ba5e3a0346ed6493725cafb1d",
263 bytes: 17_740_526,
264 }],
265 inputs: &["phase", "mask", "te", "b0", "z_prjs", "border"],
266 outputs: &["chi"],
267 size_divisor: 16,
268 },
269 ModelSpec {
270 id: "iqfm",
271 name: "iQFM",
272 stage: ModelStage::PhaseToField,
273 status: WeightStatus::Available,
274 origin: Framework::PyTorch,
275 description: "The tissue-field head of the iQSM LoT-Unet: wrapped phase → \
276 local (background-removed) field in one network (joint unwrap + \
277 BFR). Same architecture/inputs as iQSM, `lfs` weights; output is \
278 the local field (ppm), not susceptibility.",
279 paper: "Gao et al., NeuroImage 2022; doi:10.1016/j.neuroimage.2022.119410",
280 source: "https://github.com/sunhongfu/iQSM",
281 license: "",
282 files: &[WeightFile {
283 name: "iqfm.onnx",
284 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/iqfm.onnx",
285 sha256: "81965f4c0981612d979977c9618847e4cb1d340efa287b8dde894bdea5bf6ac9",
286 bytes: 17_233_763,
287 }],
288 inputs: &["phase", "mask", "te", "b0", "border"],
289 outputs: &["localfield"],
290 size_divisor: 8,
291 },
292 ModelSpec {
293 id: "nextqsm",
294 name: "NeXtQSM",
295 stage: ModelStage::SingleStep,
296 status: WeightStatus::Available,
297 origin: Framework::TensorFlow,
298 description: "U-Net background removal followed by a 6-step variational-network \
299 dipole inversion (total field → susceptibility). Reimplemented as \
300 a Rust hybrid: two exported U-Nets (`nextqsm-bf` = BFR forward, \
301 `nextqsm-vjp` = the regularizer gradient ∇ₓ mean|VarNet(x)| written \
302 as a forward graph) plus the FFT data-consistency gradient and unroll \
303 in Rust. Order the files BFR-first, VJP-second.",
304 paper: "Cognolato et al., NeuroImage 2023; doi:10.1016/j.neuroimage.2022.119729",
305 source: "https://github.com/wayne1123/NeXtQSM",
306 license: "MIT",
307 files: &[
308 WeightFile {
309 name: "nextqsm-bf.onnx",
310 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/nextqsm-bf.onnx",
311 sha256: "d0f0b0391153f8e5e07ef85d46fbe492afc8394fab008350540a6868d0b4a3ba",
312 bytes: 113_211_474,
313 },
314 WeightFile {
315 name: "nextqsm-vjp.onnx",
316 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/nextqsm-vjp.onnx",
317 sha256: "50fe388663c05b1d3996139975270e500e13feb025f1b792576dbc0da814a4bc",
318 bytes: 42_437_407,
319 },
320 ],
321 inputs: &["field", "mask", "b_vec"],
322 outputs: &["chi"],
323 size_divisor: 64,
324 },
325 ModelSpec {
326 id: "modl-qsm",
327 name: "MoDL-QSM",
328 stage: ModelStage::DipoleInversion,
329 status: WeightStatus::Available,
330 origin: Framework::TensorFlow,
331 description: "Model-based deep learning: 3-iteration unroll of a QSM \
332 forward-model gradient descent with a learned 2-channel CNN \
333 prior. The k-space dipole data-consistency (A/Aᴴ), the unroll, \
334 the learned step size and per-channel mean/std normalization run \
335 in Rust (rustfft); only the CNN prior is ONNX. Output is the STI \
336 χ33 component.",
337 paper: "Feng et al., NeuroImage 2021; doi:10.1016/j.neuroimage.2021.118376",
338 source: "https://github.com/Ruimin-Feng/MoDL-QSM",
339 license: "",
340 files: &[WeightFile {
341 name: "modl-qsm.onnx",
342 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/modl-qsm.onnx",
343 sha256: "1f8da7d39eaaa4c3f0f9e67298fba5f14230e769cfd6349c81d0b74a6f800c57",
344 bytes: 1_794_143,
345 }],
346 inputs: &["field", "mask", "b_vec"],
347 outputs: &["chi"],
348 size_divisor: 1,
349 },
350 ModelSpec {
352 id: "susep-net",
353 name: "SUSEP-Net",
354 stage: ModelStage::ChiSeparation,
355 status: WeightStatus::Available,
356 origin: Framework::PyTorch,
357 description: "Dual-branch 3D U-Net source separation from z-scored \
358 [QSM, R2', local field] → [χ+, χ−]. Normalization constants \
359 are baked into the Rust glue (SusepNetNorm).",
360 paper: "Li, Gao, Sun et al., arXiv:2506.13293 (2025)",
361 source: "https://github.com/YangGaoUQ/SUSEP-Net",
362 license: "",
363 files: &[WeightFile {
364 name: "susep-net.onnx",
365 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/susep-net.onnx",
366 sha256: "9bc85aa28451fda2c661de4b8fae8604ced7a54f028f94ed33629f6111a48b03",
367 bytes: 205_743_584,
368 }],
369 inputs: &["qsm", "r2prime", "lfs"],
370 outputs: &["chi_pos", "chi_neg"],
371 size_divisor: 8,
372 },
373 ModelSpec {
374 id: "chi-sepnet",
375 name: "χ-sepnet",
376 stage: ModelStage::ChiSeparation,
377 status: WeightStatus::Available,
378 origin: Framework::Onnx,
379 description: "SNU-LIST χ-separation network (already ONNX): a 192×192×128 \
380 3D U-Net mapping [QSM, local field, R2′/Dr] (z-scored) → [χ+, χ−], \
381 run as an overlapping sliding window. Normalization constants (Dr=114) \
382 are baked into the Rust glue (ChiSepNetNorm).",
383 paper: "Kim et al. / SNU-LIST chi-separation toolbox",
384 source: "https://github.com/SNU-LIST/chi-separation",
385 license: "",
386 files: &[WeightFile {
387 name: "chi-sepnet.onnx",
388 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/chi-sepnet.onnx",
389 sha256: "4f2343649cf36b4c9b371fc732da600720060bf2e76ed845578f16fe3411b3f2",
390 bytes: 90_314_172,
391 }],
392 inputs: &["local_field", "qsm", "r2prime"],
393 outputs: &["chi_pos", "chi_neg"],
394 size_divisor: 8,
395 },
396 ModelSpec {
397 id: "hd-bet",
398 name: "HD-BET",
399 stage: ModelStage::BrainExtraction,
400 status: WeightStatus::Available,
401 origin: Framework::PyTorch,
402 description: "nnU-Net v2 3D U-Net brain extraction (HD-BET v2): magnitude → brain \
403 mask, trained on 11,751 multi-sequence clinical MRIs. Runs at 1 mm \
404 with Gaussian-weighted 96×192×192 sliding-window patches; nnU-Net's \
405 crop/normalise/resample pipeline is in the Rust glue (bet::hd_bet). \
406 Spatial axes are dynamic (patch dims multiples of 16×32×32).",
407 paper: "Isensee et al., Hum Brain Mapp 40(17):4952-4964 (2019); https://doi.org/10.1002/hbm.24750",
408 source: "https://github.com/MIC-DKFZ/HD-BET",
409 license: "CC-BY-NC-4.0",
410 files: &[WeightFile {
411 name: "hd-bet.onnx",
412 url: "https://huggingface.co/qsmxt/qsm-onnx-weights/resolve/main/hd-bet.onnx",
413 sha256: "f15200f9a0697cf53b151e316a0c4fe2cacfdbe2220deb36cb56184be4a227f9",
414 bytes: 123_168_028,
415 }],
416 inputs: &["image"],
417 outputs: &["logits"],
418 size_divisor: 32,
419 },
420];
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425
426 #[test]
427 fn ids_are_unique_and_findable() {
428 let mut seen = std::collections::HashSet::new();
429 for m in MODELS {
430 assert!(seen.insert(m.id), "duplicate model id: {}", m.id);
431 assert!(find_model(m.id).is_some(), "find_model failed for {}", m.id);
432 assert!(!m.name.is_empty(), "{} has empty name", m.id);
433 assert!(!m.files.is_empty(), "{} has no weight files", m.id);
434 }
435 assert!(find_model("does-not-exist").is_none());
436 }
437
438 #[test]
439 fn pending_models_are_not_available() {
440 for m in MODELS.iter().filter(|m| m.status == WeightStatus::Pending) {
443 assert!(!m.is_available(), "{} is Pending but reports available", m.id);
444 assert!(
445 m.files.iter().any(|f| f.url.is_empty()),
446 "{} is Pending but every file has a url — flip status to Available",
447 m.id
448 );
449 for f in m.files.iter().filter(|f| !f.sha256.is_empty()) {
451 assert_eq!(f.sha256.len(), 64, "{}/{} sha256 must be 64 hex chars", m.id, f.name);
452 assert!(
453 f.sha256.bytes().all(|b| b.is_ascii_hexdigit()),
454 "{}/{} sha256 not hex",
455 m.id,
456 f.name
457 );
458 }
459 }
460 }
461
462 #[test]
463 fn available_models_are_fully_specified() {
464 for m in MODELS.iter().filter(|m| m.status == WeightStatus::Available) {
466 for f in m.files {
467 assert!(!f.url.is_empty(), "{}/{} available but no url", m.id, f.name);
468 assert_eq!(f.sha256.len(), 64, "{}/{} sha256 must be 64 hex chars", m.id, f.name);
469 assert!(
470 f.sha256.bytes().all(|b| b.is_ascii_hexdigit()),
471 "{}/{} sha256 not hex",
472 m.id,
473 f.name
474 );
475 }
476 assert!(m.is_available());
477 }
478 }
479}