1use crate::loader::WeightSource;
31use crate::plm_runner::{ModelMetadata, SpecialTokenLayout};
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub enum Family {
42 Esm2,
44 Amplify,
46 Esmc,
48 Esm3,
50 Mpnn,
52 T5,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum TokenizerSpec {
64 HfJson,
66 Embedded(&'static str),
69 BuiltinVocab(&'static str),
71 HfVocabTxt(VocabAlphabet),
77 None,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum VocabAlphabet {
91 SaProtPairs,
94 SingleResidue,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub enum ParityStatus {
124 Verified {
127 fixture: &'static str,
129 },
130 Unverified,
135}
136
137#[derive(Debug, Clone, Copy)]
141pub struct ModelCard {
142 pub id: &'static str,
144 pub family: Family,
146 pub source: WeightSource,
148 pub file: &'static str,
150 pub tokenizer: TokenizerSpec,
152 pub specials: SpecialTokenLayout,
154 pub metadata: ModelMetadata,
160 pub approx_bytes_f32: u64,
166 pub parity: ParityStatus,
168 pub unsupported: Option<&'static str>,
174}
175
176impl ModelCard {
177 pub const fn is_loadable(&self) -> bool {
179 self.unsupported.is_none()
180 }
181
182 pub const fn is_embedding_model(&self) -> bool {
189 !matches!(self.tokenizer, TokenizerSpec::None)
190 }
191
192 pub const fn family_str(&self) -> &'static str {
194 match self.family {
195 Family::Esm2 => "esm2",
196 Family::Amplify => "amplify",
197 Family::Esmc => "esmc",
198 Family::Esm3 => "esm3",
199 Family::Mpnn => "mpnn",
202 Family::T5 => "t5",
203 }
204 }
205}
206
207const GB: u64 = 1024 * 1024 * 1024;
208const MB: u64 = 1024 * 1024;
209
210pub const REGISTRY: &[ModelCard] = &[
216 ModelCard {
218 id: "esm2-t6-8m",
219 family: Family::Esm2,
220 source: WeightSource::safetensors("facebook/esm2_t6_8M_UR50D").at_revision("main"),
221 file: "model.safetensors",
222 tokenizer: TokenizerSpec::Embedded("esm2/tokenizer.json"),
223 specials: SpecialTokenLayout::BOS_EOS,
224 metadata: ModelMetadata {
225 d_model: 320,
226 n_layers: 6,
227 vocab_size: 33,
228 max_positions: Some(1026),
229 },
230 approx_bytes_f32: 32 * MB,
231 parity: ParityStatus::Verified {
232 fixture: "esm2_parity",
233 },
234 unsupported: None,
235 },
236 ModelCard {
237 id: "esm2-t12-35m",
238 family: Family::Esm2,
239 source: WeightSource::safetensors("facebook/esm2_t12_35M_UR50D").at_revision("main"),
240 file: "model.safetensors",
241 tokenizer: TokenizerSpec::Embedded("esm2/tokenizer.json"),
242 specials: SpecialTokenLayout::BOS_EOS,
243 metadata: ModelMetadata {
244 d_model: 480,
245 n_layers: 12,
246 vocab_size: 33,
247 max_positions: Some(1026),
248 },
249 approx_bytes_f32: 140 * MB,
250 parity: ParityStatus::Unverified,
251 unsupported: None,
252 },
253 ModelCard {
254 id: "esm2-t30-150m",
255 family: Family::Esm2,
256 source: WeightSource::safetensors("facebook/esm2_t30_150M_UR50D").at_revision("main"),
257 file: "model.safetensors",
258 tokenizer: TokenizerSpec::Embedded("esm2/tokenizer.json"),
259 specials: SpecialTokenLayout::BOS_EOS,
260 metadata: ModelMetadata {
261 d_model: 640,
262 n_layers: 30,
263 vocab_size: 33,
264 max_positions: Some(1026),
265 },
266 approx_bytes_f32: 600 * MB,
267 parity: ParityStatus::Unverified,
268 unsupported: None,
269 },
270 ModelCard {
271 id: "esm2-t33-650m",
272 family: Family::Esm2,
273 source: WeightSource::safetensors("facebook/esm2_t33_650M_UR50D").at_revision("main"),
274 file: "model.safetensors",
275 tokenizer: TokenizerSpec::Embedded("esm2/tokenizer.json"),
276 specials: SpecialTokenLayout::BOS_EOS,
277 metadata: ModelMetadata {
278 d_model: 1280,
279 n_layers: 33,
280 vocab_size: 33,
281 max_positions: Some(1026),
282 },
283 approx_bytes_f32: 2 * GB + 600 * MB,
284 parity: ParityStatus::Unverified,
285 unsupported: None,
286 },
287 ModelCard {
288 id: "esm2-t36-3b",
289 family: Family::Esm2,
290 source: WeightSource::safetensors("facebook/esm2_t36_3B_UR50D").at_revision("main"),
291 file: "model.safetensors",
292 tokenizer: TokenizerSpec::Embedded("esm2/tokenizer.json"),
293 specials: SpecialTokenLayout::BOS_EOS,
294 metadata: ModelMetadata {
295 d_model: 2560,
296 n_layers: 36,
297 vocab_size: 33,
298 max_positions: Some(1026),
299 },
300 approx_bytes_f32: 12 * GB,
301 parity: ParityStatus::Unverified,
302 unsupported: None,
303 },
304 ModelCard {
305 id: "esm2-t48-15b",
306 family: Family::Esm2,
307 source: WeightSource::safetensors("facebook/esm2_t48_15B_UR50D").at_revision("main"),
308 file: "model.safetensors",
309 tokenizer: TokenizerSpec::Embedded("esm2/tokenizer.json"),
310 specials: SpecialTokenLayout::BOS_EOS,
311 metadata: ModelMetadata {
312 d_model: 5120,
313 n_layers: 48,
314 vocab_size: 33,
315 max_positions: Some(1026),
316 },
317 approx_bytes_f32: 60 * GB,
318 parity: ParityStatus::Unverified,
319 unsupported: None,
320 },
321 ModelCard {
327 id: "esm1v-t33-650m-ur90s-1",
328 family: Family::Esm2,
329 source: WeightSource::pth("facebook/esm1v_t33_650M_UR90S_1", None),
330 file: "pytorch_model.bin",
331 tokenizer: TokenizerSpec::Embedded("esm2/tokenizer.json"),
334 specials: SpecialTokenLayout::BOS_EOS,
335 metadata: ModelMetadata {
336 d_model: 1280,
337 n_layers: 33,
338 vocab_size: 33,
339 max_positions: Some(1026),
340 },
341 approx_bytes_f32: 2 * GB + 600 * MB,
342 parity: ParityStatus::Unverified,
343 unsupported: None,
344 },
345 ModelCard {
346 id: "esm1v-t33-650m-ur90s-2",
347 family: Family::Esm2,
348 source: WeightSource::pth("facebook/esm1v_t33_650M_UR90S_2", None),
349 file: "pytorch_model.bin",
350 tokenizer: TokenizerSpec::Embedded("esm2/tokenizer.json"),
353 specials: SpecialTokenLayout::BOS_EOS,
354 metadata: ModelMetadata {
355 d_model: 1280,
356 n_layers: 33,
357 vocab_size: 33,
358 max_positions: Some(1026),
359 },
360 approx_bytes_f32: 2 * GB + 600 * MB,
361 parity: ParityStatus::Unverified,
362 unsupported: None,
363 },
364 ModelCard {
365 id: "esm1v-t33-650m-ur90s-3",
366 family: Family::Esm2,
367 source: WeightSource::pth("facebook/esm1v_t33_650M_UR90S_3", None),
368 file: "pytorch_model.bin",
369 tokenizer: TokenizerSpec::Embedded("esm2/tokenizer.json"),
372 specials: SpecialTokenLayout::BOS_EOS,
373 metadata: ModelMetadata {
374 d_model: 1280,
375 n_layers: 33,
376 vocab_size: 33,
377 max_positions: Some(1026),
378 },
379 approx_bytes_f32: 2 * GB + 600 * MB,
380 parity: ParityStatus::Unverified,
381 unsupported: None,
382 },
383 ModelCard {
384 id: "esm1v-t33-650m-ur90s-4",
385 family: Family::Esm2,
386 source: WeightSource::pth("facebook/esm1v_t33_650M_UR90S_4", None),
387 file: "pytorch_model.bin",
388 tokenizer: TokenizerSpec::Embedded("esm2/tokenizer.json"),
391 specials: SpecialTokenLayout::BOS_EOS,
392 metadata: ModelMetadata {
393 d_model: 1280,
394 n_layers: 33,
395 vocab_size: 33,
396 max_positions: Some(1026),
397 },
398 approx_bytes_f32: 2 * GB + 600 * MB,
399 parity: ParityStatus::Unverified,
400 unsupported: None,
401 },
402 ModelCard {
403 id: "esm1v-t33-650m-ur90s-5",
404 family: Family::Esm2,
405 source: WeightSource::pth("facebook/esm1v_t33_650M_UR90S_5", None),
406 file: "pytorch_model.bin",
407 tokenizer: TokenizerSpec::Embedded("esm2/tokenizer.json"),
410 specials: SpecialTokenLayout::BOS_EOS,
411 metadata: ModelMetadata {
412 d_model: 1280,
413 n_layers: 33,
414 vocab_size: 33,
415 max_positions: Some(1026),
416 },
417 approx_bytes_f32: 2 * GB + 600 * MB,
418 parity: ParityStatus::Unverified,
419 unsupported: None,
420 },
421 ModelCard {
422 id: "esm1b-t33-650m-ur50s",
423 family: Family::Esm2,
424 source: WeightSource::pth("facebook/esm1b_t33_650M_UR50S", None),
425 file: "pytorch_model.bin",
426 tokenizer: TokenizerSpec::Embedded("esm2/tokenizer.json"),
427 specials: SpecialTokenLayout::BOS_EOS,
428 metadata: ModelMetadata {
429 d_model: 1280,
430 n_layers: 33,
431 vocab_size: 33,
432 max_positions: Some(1026),
433 },
434 approx_bytes_f32: 2 * GB + 600 * MB,
435 parity: ParityStatus::Unverified,
436 unsupported: None,
437 },
438 ModelCard {
440 id: "saprot-35m-af2",
441 family: Family::Esm2,
442 source: WeightSource::pth("westlake-repl/SaProt_35M_AF2", None),
443 file: "pytorch_model.bin",
445 tokenizer: TokenizerSpec::HfVocabTxt(VocabAlphabet::SaProtPairs),
446 specials: SpecialTokenLayout::BOS_EOS,
447 metadata: ModelMetadata {
448 d_model: 480,
449 n_layers: 12,
450 vocab_size: 446,
452 max_positions: Some(1026),
453 },
454 approx_bytes_f32: 140 * MB,
455 parity: ParityStatus::Verified {
456 fixture: "saprot_parity",
457 },
458 unsupported: None,
459 },
460 ModelCard {
461 id: "saprot-650m-af2",
462 family: Family::Esm2,
463 source: WeightSource::pth("westlake-repl/SaProt_650M_AF2", None),
464 file: "pytorch_model.bin",
465 tokenizer: TokenizerSpec::HfVocabTxt(VocabAlphabet::SaProtPairs),
466 specials: SpecialTokenLayout::BOS_EOS,
467 metadata: ModelMetadata {
468 d_model: 1280,
469 n_layers: 33,
470 vocab_size: 446,
471 max_positions: Some(1026),
472 },
473 approx_bytes_f32: 2 * GB + 600 * MB,
474 parity: ParityStatus::Unverified,
475 unsupported: None,
476 },
477 ModelCard {
485 id: "fastesm2-650",
486 family: Family::Esm2,
487 source: WeightSource::safetensors("Synthyra/FastESM2_650"),
490 file: "model.safetensors",
491 tokenizer: TokenizerSpec::Embedded("esm2/tokenizer.json"),
492 specials: SpecialTokenLayout::BOS_EOS,
493 metadata: ModelMetadata {
494 d_model: 1280,
495 n_layers: 33,
496 vocab_size: 33,
497 max_positions: Some(1026),
498 },
499 approx_bytes_f32: 2 * GB + 600 * MB,
500 parity: ParityStatus::Verified {
501 fixture: "fastesm2_parity",
502 },
503 unsupported: None,
504 },
505 ModelCard {
506 id: "pepmlm-650m",
507 family: Family::Esm2,
508 source: WeightSource::pth("ChatterjeeLab/PepMLM-650M", None),
511 file: "pytorch_model.bin",
512 tokenizer: TokenizerSpec::Embedded("esm2/tokenizer.json"),
513 specials: SpecialTokenLayout::BOS_EOS,
514 metadata: ModelMetadata {
515 d_model: 1280,
516 n_layers: 33,
517 vocab_size: 33,
518 max_positions: Some(1026),
519 },
520 approx_bytes_f32: 2 * GB + 600 * MB,
521 parity: ParityStatus::Verified {
522 fixture: "pepmlm_parity",
523 },
524 unsupported: None,
525 },
526 ModelCard {
527 id: "dplm-650m",
528 family: Family::Esm2,
529 source: WeightSource::pth("airkingbd/dplm_650m", None),
535 file: "pytorch_model.bin",
536 tokenizer: TokenizerSpec::Embedded("esm2/tokenizer.json"),
537 specials: SpecialTokenLayout::BOS_EOS,
538 metadata: ModelMetadata {
539 d_model: 1280,
540 n_layers: 33,
541 vocab_size: 33,
542 max_positions: Some(1026),
543 },
544 approx_bytes_f32: 2 * GB + 600 * MB,
545 parity: ParityStatus::Verified {
546 fixture: "dplm_parity",
547 },
548 unsupported: None,
549 },
550 ModelCard {
552 id: "amplify-120m",
553 family: Family::Amplify,
554 source: WeightSource::safetensors("chandar-lab/AMPLIFY_120M").at_revision("main"),
555 file: "model.safetensors",
556 tokenizer: TokenizerSpec::HfJson,
559 specials: SpecialTokenLayout::BOS_EOS,
560 metadata: ModelMetadata {
561 d_model: 640,
562 n_layers: 24,
563 vocab_size: 27,
564 max_positions: Some(2048),
565 },
566 approx_bytes_f32: 480 * MB,
567 parity: ParityStatus::Verified {
568 fixture: "amplify_parity",
569 },
570 unsupported: None,
571 },
572 ModelCard {
573 id: "amplify-350m",
574 family: Family::Amplify,
575 source: WeightSource::safetensors("chandar-lab/AMPLIFY_350M").at_revision("main"),
576 file: "model.safetensors",
577 tokenizer: TokenizerSpec::HfJson,
578 specials: SpecialTokenLayout::BOS_EOS,
579 metadata: ModelMetadata {
580 d_model: 960,
581 n_layers: 32,
582 vocab_size: 27,
583 max_positions: Some(2048),
584 },
585 approx_bytes_f32: GB + 400 * MB,
586 parity: ParityStatus::Unverified,
587 unsupported: None,
588 },
589 ModelCard {
591 id: "esmc-300m",
592 family: Family::Esmc,
593 source: WeightSource::pth("EvolutionaryScale/esmc-300m-2024-12", None),
594 file: "data/weights/esmc_300m_2024_12_v0.pth",
595 tokenizer: TokenizerSpec::BuiltinVocab("esmc::tokenizer::EsmSequenceTokenizer"),
596 specials: SpecialTokenLayout::BOS_EOS,
597 metadata: ModelMetadata {
598 d_model: 960,
599 n_layers: 30,
600 vocab_size: 64,
601 max_positions: None,
603 },
604 approx_bytes_f32: GB + 200 * MB,
605 parity: ParityStatus::Verified {
606 fixture: "esmc_parity",
607 },
608 unsupported: None,
609 },
610 ModelCard {
611 id: "esmc-600m",
612 family: Family::Esmc,
613 source: WeightSource::pth("EvolutionaryScale/esmc-600m-2024-12", None),
614 file: "data/weights/esmc_600m_2024_12_v0.pth",
615 tokenizer: TokenizerSpec::BuiltinVocab("esmc::tokenizer::EsmSequenceTokenizer"),
616 specials: SpecialTokenLayout::BOS_EOS,
617 metadata: ModelMetadata {
618 d_model: 1152,
619 n_layers: 36,
620 vocab_size: 64,
621 max_positions: None,
622 },
623 approx_bytes_f32: 2 * GB + 400 * MB,
624 parity: ParityStatus::Unverified,
625 unsupported: None,
626 },
627 ModelCard {
628 id: "esmc-6b",
629 family: Family::Esmc,
630 source: WeightSource::safetensors("EvolutionaryScale/esmc-6b-2024-12"),
631 file: "model.safetensors.index.json",
634 tokenizer: TokenizerSpec::BuiltinVocab("esmc::tokenizer::EsmSequenceTokenizer"),
635 specials: SpecialTokenLayout::BOS_EOS,
636 metadata: ModelMetadata {
637 d_model: 2560,
638 n_layers: 80,
639 vocab_size: 64,
640 max_positions: None,
641 },
642 approx_bytes_f32: 24 * GB,
643 parity: ParityStatus::Unverified,
644 unsupported: None,
645 },
646 ModelCard {
648 id: "esm3-sm-open-v1",
649 family: Family::Esm3,
650 source: WeightSource::pth("EvolutionaryScale/esm3-sm-open-v1", None),
651 file: "data/weights/esm3_sm_open_v1.pth",
652 tokenizer: TokenizerSpec::BuiltinVocab("esm3::tokenization::sequence"),
653 specials: SpecialTokenLayout::BOS_EOS,
654 metadata: ModelMetadata {
655 d_model: 1536,
656 n_layers: 48,
657 vocab_size: 64,
658 max_positions: None,
659 },
660 approx_bytes_f32: 5 * GB + 600 * MB,
661 parity: ParityStatus::Verified {
662 fixture: "esm3_parity",
663 },
664 unsupported: None,
665 },
666 ModelCard {
667 id: "esm3-structure-encoder-v0",
668 family: Family::Esm3,
669 source: WeightSource::pth("EvolutionaryScale/esm3-sm-open-v1", None),
670 file: "data/weights/esm3_structure_encoder_v0.pth",
671 tokenizer: TokenizerSpec::None,
673 specials: SpecialTokenLayout::NONE,
674 metadata: ModelMetadata {
675 d_model: 1024,
676 n_layers: 2,
677 vocab_size: 4096,
679 max_positions: None,
680 },
681 approx_bytes_f32: 30 * MB,
682 parity: ParityStatus::Verified {
683 fixture: "esm3_structure_parity",
684 },
685 unsupported: None,
686 },
687 ModelCard {
690 id: "prott5-xl-half-uniref50-enc",
691 family: Family::T5,
692 source: WeightSource::pth("Rostlab/prot_t5_xl_half_uniref50-enc", None).at_revision("main"),
695 file: "pytorch_model.bin",
696 tokenizer: TokenizerSpec::BuiltinVocab("t5::tokenizer"),
700 specials: SpecialTokenLayout::EOS_ONLY,
703 metadata: ModelMetadata {
704 d_model: 1024,
705 n_layers: 24,
706 vocab_size: 128,
709 max_positions: None,
711 },
712 approx_bytes_f32: 4800 * MB,
716 parity: ParityStatus::Verified {
717 fixture: "prott5_parity",
718 },
719 unsupported: None,
720 },
721 ModelCard {
729 id: "ankh-base",
730 family: Family::T5,
731 source: WeightSource::pth("ElnaggarLab/ankh-base", None).at_revision("main"),
733 file: "pytorch_model.bin",
734 tokenizer: TokenizerSpec::BuiltinVocab("t5::tokenizer"),
740 specials: SpecialTokenLayout::EOS_ONLY,
741 metadata: ModelMetadata {
742 d_model: 768,
743 n_layers: 48,
746 vocab_size: 144,
747 max_positions: None,
749 },
750 approx_bytes_f32: 2950 * MB,
751 parity: ParityStatus::Verified {
752 fixture: "ankh_parity",
753 },
754 unsupported: None,
755 },
756 ModelCard {
757 id: "ankh-large",
758 family: Family::T5,
759 source: WeightSource::pth("ElnaggarLab/ankh-large", None).at_revision("main"),
760 file: "pytorch_model.bin",
761 tokenizer: TokenizerSpec::BuiltinVocab("t5::tokenizer"),
762 specials: SpecialTokenLayout::EOS_ONLY,
763 metadata: ModelMetadata {
764 d_model: 1536,
765 n_layers: 48,
766 vocab_size: 144,
767 max_positions: None,
768 },
769 approx_bytes_f32: 7520 * MB,
770 parity: ParityStatus::Unverified,
771 unsupported: None,
772 },
773 ModelCard {
780 id: "prostt5-fp16",
781 family: Family::T5,
782 source: WeightSource::pth("Rostlab/ProstT5_fp16", None).at_revision("main"),
784 file: "pytorch_model.bin",
785 tokenizer: TokenizerSpec::None,
790 specials: SpecialTokenLayout::NONE,
791 metadata: ModelMetadata {
792 d_model: 1024,
793 n_layers: 24,
794 vocab_size: 150,
797 max_positions: None,
798 },
799 approx_bytes_f32: 11280 * MB,
800 parity: ParityStatus::Verified {
801 fixture: "prostt5_parity",
802 },
803 unsupported: None,
804 },
805 ModelCard {
807 id: "proteinmpnn-v48-020",
808 family: Family::Mpnn,
809 source: WeightSource::pth("zcpbx/ligandmpnn-weights", Some("model_state_dict"))
810 .at_revision("main"),
811 file: "model_params/proteinmpnn_v_48_020.pt",
812 tokenizer: TokenizerSpec::None,
814 specials: SpecialTokenLayout::NONE,
815 metadata: ModelMetadata {
816 d_model: 128,
817 n_layers: 6,
819 vocab_size: 21,
821 max_positions: None,
822 },
823 approx_bytes_f32: 7 * MB,
824 parity: ParityStatus::Verified {
825 fixture: "proteinmpnn_parity",
826 },
827 unsupported: None,
828 },
829 ModelCard {
830 id: "ligandmpnn-v32-020-25",
831 family: Family::Mpnn,
832 source: WeightSource::pth("zcpbx/ligandmpnn-weights", Some("model_state_dict"))
833 .at_revision("main"),
834 file: "model_params/ligandmpnn_v_32_020_25.pt",
835 tokenizer: TokenizerSpec::None,
837 specials: SpecialTokenLayout::NONE,
838 metadata: ModelMetadata {
839 d_model: 128,
840 n_layers: 10,
843 vocab_size: 21,
844 max_positions: None,
845 },
846 approx_bytes_f32: 11 * MB,
847 parity: ParityStatus::Verified {
848 fixture: "ligandmpnn_parity",
849 },
850 unsupported: None,
851 },
852];
853
854pub fn support_matrix_markdown() -> String {
868 let mut out = String::new();
869 out.push_str("| Model | Family | Weights | Parity | Status |\n");
870 out.push_str("|---|---|---|---|---|\n");
871
872 for card in REGISTRY {
873 let format = match card.source.format {
874 crate::loader::Format::Safetensors => "safetensors",
875 crate::loader::Format::Pth { .. } => "pth",
876 };
877 let parity = match card.parity {
878 ParityStatus::Verified { fixture } => {
879 format!("verified (`{fixture}`)")
880 }
881 ParityStatus::Unverified => "**not checked**".to_string(),
882 };
883 let status = match card.unsupported {
884 None => "supported".to_string(),
885 Some(reason) => {
886 let short = reason.split(" (ferritin-").next().unwrap_or(reason);
888 let issue = reason
889 .rsplit_once("(ferritin-")
890 .map(|(_, tail)| tail.trim_end_matches(')'))
891 .unwrap_or("");
892 let first = short.split(&[',', ':'][..]).next().unwrap_or(short);
893 if issue.is_empty() {
894 format!("**unsupported** — {first}")
895 } else {
896 format!("**unsupported** — {first} (ferritin-{issue})")
897 }
898 }
899 };
900 out.push_str(&format!(
901 "| `{}` | {:?} | `{}` ({format}) | {parity} | {status} |\n",
902 card.id, card.family, card.source.repo_id,
903 ));
904 }
905 out
906}
907
908pub fn lookup(id: &str) -> Option<&'static ModelCard> {
912 REGISTRY.iter().find(|c| c.id == id)
913}
914
915pub fn by_family(family: Family) -> impl Iterator<Item = &'static ModelCard> {
917 REGISTRY.iter().filter(move |c| c.family == family)
918}
919
920pub fn loadable() -> impl Iterator<Item = &'static ModelCard> {
922 REGISTRY.iter().filter(|c| c.is_loadable())
923}
924
925#[cfg(test)]
928mod tests {
929 use super::*;
930 use std::collections::HashSet;
931
932 #[test]
933 fn test_ids_are_unique() {
934 let mut seen = HashSet::new();
935 for card in REGISTRY {
936 assert!(seen.insert(card.id), "duplicate registry id: {}", card.id);
937 }
938 }
939
940 #[test]
943 fn test_ids_are_kebab_case() {
944 for card in REGISTRY {
945 assert!(
946 card.id
947 .chars()
948 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'),
949 "id {:?} should be lowercase kebab-case",
950 card.id
951 );
952 }
953 }
954
955 #[test]
958 fn test_every_source_repo_is_well_formed() {
959 for card in REGISTRY {
960 assert!(
961 card.source.repo_id.split('/').count() == 2
962 && !card.source.repo_id.starts_with('/')
963 && !card.source.repo_id.ends_with('/'),
964 "{}: malformed repo id {:?}",
965 card.id,
966 card.source.repo_id
967 );
968 assert!(!card.file.is_empty(), "{}: empty weight filename", card.id);
969 }
970 }
971
972 #[test]
973 fn test_metadata_dimensions_are_plausible() {
974 for card in REGISTRY {
975 assert!(card.metadata.d_model > 0, "{}: zero d_model", card.id);
976 assert!(card.metadata.n_layers > 0, "{}: zero n_layers", card.id);
977 assert!(
978 card.approx_bytes_f32 > 0,
979 "{}: zero approx_bytes_f32",
980 card.id
981 );
982 }
983 }
984
985 #[test]
988 fn test_embedding_models_have_a_tokenizer_and_vocab() {
989 for card in REGISTRY {
990 if card.is_embedding_model() {
991 assert_ne!(
992 card.tokenizer,
993 TokenizerSpec::None,
994 "{}: an embedding model needs a tokenizer",
995 card.id
996 );
997 assert!(
998 card.metadata.vocab_size > 0,
999 "{}: an embedding model needs a vocabulary",
1000 card.id
1001 );
1002 } else {
1003 assert_eq!(
1004 card.metadata.max_positions, None,
1005 "{}: a structure model has no token positions to cap",
1006 card.id
1007 );
1008 }
1009 }
1010 }
1011
1012 #[test]
1016 fn test_every_family_has_at_least_one_model() {
1017 for family in [
1018 Family::Esm2,
1019 Family::Amplify,
1020 Family::Esmc,
1021 Family::Esm3,
1022 Family::Mpnn,
1023 ] {
1024 assert!(
1025 by_family(family).next().is_some(),
1026 "{family:?} has no models; drop the variant or add its loader"
1027 );
1028 }
1029 }
1030
1031 #[test]
1035 fn test_unsupported_models_are_the_known_set() {
1036 let mut unsupported: Vec<&str> = REGISTRY
1037 .iter()
1038 .filter(|c| !c.is_loadable())
1039 .map(|c| c.id)
1040 .collect();
1041 unsupported.sort_unstable();
1042 assert!(
1046 unsupported.is_empty(),
1047 "every registered model should load; got {unsupported:?}"
1048 );
1049
1050 for card in REGISTRY.iter().filter(|c| !c.is_loadable()) {
1051 let reason = card.unsupported.unwrap();
1052 assert!(
1053 reason.contains("ferritin-"),
1054 "{}: an unsupported reason should cite its tracking issue; got: {reason}",
1055 card.id
1056 );
1057 }
1058 }
1059
1060 #[test]
1077 fn test_verified_models_name_a_real_fixture() {
1078 let mut verified: Vec<(&str, &str)> = REGISTRY
1079 .iter()
1080 .filter_map(|c| match c.parity {
1081 ParityStatus::Verified { fixture } => Some((c.id, fixture)),
1082 ParityStatus::Unverified => None,
1083 })
1084 .collect();
1085 verified.sort_unstable();
1086 assert_eq!(
1087 verified,
1088 [
1089 ("amplify-120m", "amplify_parity"),
1090 ("ankh-base", "ankh_parity"),
1091 ("dplm-650m", "dplm_parity"),
1092 ("esm2-t6-8m", "esm2_parity"),
1093 ("esm3-sm-open-v1", "esm3_parity"),
1094 ("esm3-structure-encoder-v0", "esm3_structure_parity"),
1095 ("esmc-300m", "esmc_parity"),
1096 ("fastesm2-650", "fastesm2_parity"),
1097 ("ligandmpnn-v32-020-25", "ligandmpnn_parity"),
1098 ("pepmlm-650m", "pepmlm_parity"),
1099 ("prostt5-fp16", "prostt5_parity"),
1100 ("proteinmpnn-v48-020", "proteinmpnn_parity"),
1101 ("prott5-xl-half-uniref50-enc", "prott5_parity"),
1102 ("saprot-35m-af2", "saprot_parity"),
1103 ],
1104 "the set of parity-verified models changed; that is a deliberate act"
1105 );
1106 }
1107
1108 #[test]
1109 fn test_lookup_finds_and_misses() {
1110 assert_eq!(lookup("esm2-t6-8m").map(|c| c.id), Some("esm2-t6-8m"));
1111 assert!(lookup("no-such-model").is_none());
1112 }
1113
1114 #[test]
1115 fn test_loadable_excludes_unsupported() {
1116 assert!(loadable().all(|c| c.unsupported.is_none()));
1117 assert_eq!(
1118 loadable().count(),
1119 REGISTRY.len(),
1120 "no model is currently unsupported, so loadable() should be every row"
1121 );
1122 }
1123}
1124
1125#[cfg(test)]
1126mod matrix {
1127 use super::*;
1128
1129 #[test]
1135 #[ignore = "prints the matrix for copying into lib.rs"]
1136 fn print_support_matrix() {
1137 println!("{}", support_matrix_markdown());
1138 }
1139
1140 #[test]
1145 fn test_lib_rs_support_matrix_is_current() {
1146 const LIB_RS: &str = include_str!("lib.rs");
1147 const BEGIN: &str = "//! <!-- BEGIN SUPPORT MATRIX -->";
1148 const END: &str = "//! <!-- END SUPPORT MATRIX -->";
1149
1150 let start = LIB_RS
1151 .find(BEGIN)
1152 .expect("lib.rs should carry a BEGIN SUPPORT MATRIX marker")
1153 + BEGIN.len();
1154 let end = LIB_RS
1155 .find(END)
1156 .expect("lib.rs should carry an END SUPPORT MATRIX marker");
1157
1158 let embedded: String = LIB_RS[start..end]
1159 .lines()
1160 .filter(|l| !l.trim().is_empty())
1161 .map(|l| {
1162 format!(
1163 "{}\n",
1164 l.trim_start().trim_start_matches("//!").trim_start()
1165 )
1166 })
1167 .collect();
1168
1169 let rendered: String = support_matrix_markdown()
1170 .lines()
1171 .map(|l| format!("{l}\n"))
1172 .collect();
1173
1174 assert_eq!(
1175 embedded, rendered,
1176 "the support matrix in lib.rs is stale. Regenerate it with:\n \
1177 cargo test -p ferritin-plms --lib print_support_matrix -- --ignored --nocapture\n\
1178 then replace the block between the SUPPORT MATRIX markers."
1179 );
1180 }
1181
1182 #[test]
1184 fn test_matrix_states_parity_for_every_model() {
1185 let matrix = support_matrix_markdown();
1186 for card in REGISTRY {
1187 let row = matrix
1188 .lines()
1189 .find(|l| l.contains(&format!("`{}`", card.id)))
1190 .unwrap_or_else(|| panic!("{} missing from the matrix", card.id));
1191 assert!(
1192 row.contains("verified") || row.contains("not checked"),
1193 "{}: the matrix must state a parity verdict; got: {row}",
1194 card.id
1195 );
1196 }
1197 }
1198}