descriptron-core 2.7.5__tar.gz → 2.7.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/PKG-INFO +1 -1
  2. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/pyproject.toml +1 -1
  3. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_coded_states_from_coco_v1.py +45 -9
  4. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_confabulation_checker_v2.py +2 -1
  5. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_description_refiner_v1.py +2 -1
  6. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_key_builder_v1.py +1 -0
  7. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_key_feature_filter_v2.py +17 -0
  8. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/compile_specimen_data.py +4 -1
  9. descriptron_core-2.7.8/src/descriptron_core/tools/descriptron_check_completeness_v1.py +375 -0
  10. descriptron_core-2.7.8/src/descriptron_core/tools/descriptron_community_v1.py +909 -0
  11. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/landmark_gpa_V2.py +29 -4
  12. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/run_full_pipeline_v2.py +46 -1
  13. descriptron_core-2.7.8/src/descriptron_core/tools/validate_community_run_vs_r.py +159 -0
  14. descriptron_core-2.7.8/src/descriptron_core/tools/validate_community_vs_r.py +107 -0
  15. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/.gitignore +0 -0
  16. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/LICENSE +0 -0
  17. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/NOTICE +0 -0
  18. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/README.md +0 -0
  19. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/__init__.py +0 -0
  20. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/cli.py +0 -0
  21. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/data/biorag_prompts/biorag_system_prompts_v1.txt +0 -0
  22. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/data/biorag_prompts/biorag_system_prompts_v2.txt +0 -0
  23. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/data/biorag_prompts/diaphorina_user_prompts.txt +0 -0
  24. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/data/biorag_prompts/ontologies/aism.obo +0 -0
  25. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/data/biorag_prompts/ontologies/pato.obo +0 -0
  26. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/SDD.xsd +0 -0
  27. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/SDD_EnumLib.xsd +0 -0
  28. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/SDD_TypeLib.xsd +0 -0
  29. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_BasePattern.xsd +0 -0
  30. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_CoreExtensions.xsd +0 -0
  31. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_CoreOntology.xsd +0 -0
  32. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_EnumLib.xsd +0 -0
  33. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_LinneanCore.xsd +0 -0
  34. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_TypeLib.xsd +0 -0
  35. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/w3c-schemata/xml.xsd +0 -0
  36. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/data/biorag_prompts/schemas/taxpub/sample_treatment_deposit_example.xml +0 -0
  37. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/data/biorag_prompts/schemas/taxpub/sample_zookeys_26056_tp.xml +0 -0
  38. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/data/biorag_prompts/schemas/taxpub/tax-treatment-NS0-v1_flat.dtd +0 -0
  39. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/data/biorag_prompts/taxon_profiles/TEMPLATE_taxon_profile.yaml +0 -0
  40. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/data/biorag_prompts/taxon_profiles/diaphorina_taxon_profile.yaml +0 -0
  41. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/FHS_and_CLAHE_V21.py +0 -0
  42. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_add_discrete_characters_v1.py +0 -0
  43. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_annotation_reorient_v1.py +0 -0
  44. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_annotation_screen_v1.py +0 -0
  45. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_autapomorphy_v1.py +0 -0
  46. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_calibrate_v1.py +0 -0
  47. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_calibration_figures_v1.py +0 -0
  48. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_character_gate_v1.py +0 -0
  49. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_character_reliability_v1.py +0 -0
  50. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_character_robustness_v1.py +0 -0
  51. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_collaborator_forms_v1.py +0 -0
  52. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_computed_sets_v1.py +0 -0
  53. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_congruence_compare_v1.py +0 -0
  54. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_descriptive_scoring_v1.py +0 -0
  55. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_feature_policy.py +0 -0
  56. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_frame_orientation_calibrate_v1.py +0 -0
  57. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_frame_orientation_v1.py +0 -0
  58. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_gate_compare_v1.py +0 -0
  59. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_graph_identify_v1.py +0 -0
  60. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_graph_support_figure_v1.py +0 -0
  61. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_homology_frame_v1.py +0 -0
  62. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_instrument_compare_v3.py +0 -0
  63. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_key_fuzzy_figures_v1.py +0 -0
  64. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_key_fuzzy_v1.py +0 -0
  65. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_key_holdout_v1.py +0 -0
  66. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_key_jackknife_support_v1.py +0 -0
  67. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_label_conflict_check_v1.py +0 -0
  68. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_llm_backend.py +0 -0
  69. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_make_taxon_profile_v1.py +0 -0
  70. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_methods_summary_figure_v1.py +0 -0
  71. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_novelty_score_v1.py +0 -0
  72. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_ontology_annotator_v2.py +0 -0
  73. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_outgroup_batch_check_v1.py +0 -0
  74. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_outline_shape_v1.py +0 -0
  75. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_prompts/biorag_system_prompts_v1.txt +0 -0
  76. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_prompts/biorag_system_prompts_v2.txt +0 -0
  77. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_prompts/diaphorina_user_prompts.txt +0 -0
  78. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_prompts/ontologies/aism.obo +0 -0
  79. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_prompts/ontologies/pato.obo +0 -0
  80. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/SDD.xsd +0 -0
  81. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/SDD_EnumLib.xsd +0 -0
  82. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/SDD_TypeLib.xsd +0 -0
  83. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_BasePattern.xsd +0 -0
  84. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_CoreExtensions.xsd +0 -0
  85. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_CoreOntology.xsd +0 -0
  86. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_EnumLib.xsd +0 -0
  87. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_LinneanCore.xsd +0 -0
  88. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_TypeLib.xsd +0 -0
  89. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/w3c-schemata/xml.xsd +0 -0
  90. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_prompts/schemas/taxpub/sample_treatment_deposit_example.xml +0 -0
  91. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_prompts/schemas/taxpub/sample_zookeys_26056_tp.xml +0 -0
  92. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_prompts/schemas/taxpub/tax-treatment-NS0-v1_flat.dtd +0 -0
  93. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_prompts/taxon_profiles/TEMPLATE_taxon_profile.yaml +0 -0
  94. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_prompts/taxon_profiles/diaphorina_taxon_profile.yaml +0 -0
  95. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_provenance_v1.py +0 -0
  96. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_rebuild_downstream_v1.sh +0 -0
  97. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_reliability_figures_v1.py +0 -0
  98. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_rename_species_v1.py +0 -0
  99. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_rerun_descriptive_tables_v1.sh +0 -0
  100. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_retest_compare_v1.py +0 -0
  101. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_specimen_id.py +0 -0
  102. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_subjective_checks_v1.py +0 -0
  103. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_type_material_v1.py +0 -0
  104. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_vlm_characters_v1.py +0 -0
  105. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_vlm_heatmap_figures_v1.py +0 -0
  106. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_vlm_reliability_figure_v1.py +0 -0
  107. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biorag_vlm_roi_figures_v1.py +0 -0
  108. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biosyslit_rag_retrieval.py +0 -0
  109. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/biosyslit_rag_retrieval_v2.py +0 -0
  110. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/build_monograph_exports_v1.py +0 -0
  111. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/build_species_treatment_docx_v2.py +0 -0
  112. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/coco_combiner_V13.py +0 -0
  113. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/coco_converter_v24.py +0 -0
  114. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/coi_species_tree_v1.py +0 -0
  115. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/color_extraction_with_names-v8.py +0 -0
  116. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/color_phenomics_homology_v2_1.py +0 -0
  117. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/descriptron_centerline.py +0 -0
  118. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/descriptron_character_signal_v1.py +0 -0
  119. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/descriptron_check_cross_image_copies_v1.py +0 -0
  120. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/descriptron_coco_tools.py +0 -0
  121. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/descriptron_convert.py +0 -0
  122. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/descriptron_credentials.py +0 -0
  123. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/descriptron_descriptive_characters.json +0 -0
  124. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/descriptron_joints.py +0 -0
  125. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/descriptron_labelled_plates_v1.py +0 -0
  126. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/descriptron_metadata.py +0 -0
  127. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/descriptron_phylo.py +0 -0
  128. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/descriptron_phylo_figure_v1.py +0 -0
  129. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/descriptron_reexamine_v1.py +0 -0
  130. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/descriptron_reexamine_v2.py +0 -0
  131. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/descriptron_rosetta.py +0 -0
  132. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/descriptron_shape_stats.py +0 -0
  133. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/descriptron_trait_stats.py +0 -0
  134. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/dna_vs_morphology_v1.py +0 -0
  135. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/generate_species_plates.py +0 -0
  136. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/landmark_gpa_V1.py +0 -0
  137. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/make_reproducibility_bundle.py +0 -0
  138. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/semi_landmark_and_kpts_procrustesV34_GPA.py +0 -0
  139. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/semi_landmark_and_kpts_procrustesV42_GPA.py +0 -0
  140. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/texture_phenomics_from_coco.py +0 -0
  141. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/texture_phenomics_homology.py +0 -0
  142. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/validate_centerline_lengths.py +0 -0
  143. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/validate_phylo_published_waldron2025.py +0 -0
  144. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/validate_phylo_real_tree.py +0 -0
  145. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/validate_phylo_traits.py +0 -0
  146. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/validate_pipeline_gm_vs_geomorph.py +0 -0
  147. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/validate_semilandmarks_vs_geomorph.py +0 -0
  148. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/validate_shape_stats_vs_geomorph.py +0 -0
  149. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/validate_shape_stats_vs_rrpp.py +0 -0
  150. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/validate_texture_glcm.py +0 -0
  151. {descriptron_core-2.7.5 → descriptron_core-2.7.8}/src/descriptron_core/tools/validate_trait_stats.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: descriptron-core
3
- Version: 2.7.5
3
+ Version: 2.7.8
4
4
  Summary: Evidence-tiered species descriptions, keys and audits from phenomic matrices
5
5
  Project-URL: Homepage, https://github.com/alexrvandam/Descriptron
6
6
  Project-URL: Repository, https://github.com/alexrvandam/Descriptron
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "descriptron-core"
7
- version = "2.7.5"
7
+ version = "2.7.8"
8
8
  description = "Evidence-tiered species descriptions, keys and audits from phenomic matrices"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -18,6 +18,12 @@ script turns them into what the pipeline reads:
18
18
  count. A character scored in fewer than --min_species species, or with one state everywhere, is dropped
19
19
  and reported, as in biorag_add_discrete_characters_v1.
20
20
 
21
+ Structures recorded as ABSENT in the GUI (v84: Record absent -> Absent, or a Lost keypoint; images[].structure_status
22
+ and annotations[].keypoint_status) become a character 'presence' of that structure: present where a mask or a
23
+ visible keypoint exists, absent where recorded. Only structures with at least one recorded absence get it, and a
24
+ structure that is merely missing (not visible, or never annotated) is missing data, never 'absent'
25
+ (descriptron_check_completeness_v1 lists those gaps).
26
+
21
27
  States a taxonomist records are bench observations, so they enter at --tier key by default (model-scored
22
28
  states stay at the tier their reliability check allows). The source matrix folder is never written to.
23
29
 
@@ -38,7 +44,7 @@ from pathlib import Path
38
44
  import pandas as pd
39
45
 
40
46
  sys.path.insert(0, str(Path(__file__).resolve().parent))
41
- VERSION = "1.0"
47
+ VERSION = "1.1" # 1.1: presence / absence of structures recorded as absent
42
48
 
43
49
 
44
50
  def slug(s):
@@ -87,12 +93,27 @@ def read_states(coco_paths, group_labels, profile, vocab):
87
93
  cat = cats.get(a.get("category_id"), str(a.get("category_id")))
88
94
  cat = aliases.get(cat, cat)
89
95
  for ch, st in attrs.items():
90
- if str(ch).startswith("_") or st in (None, ""):
91
- continue
96
+ if str(ch).startswith("_") or str(ch).startswith("via_") or st in (None, ""):
97
+ continue # 1.1: via_* = VIA annotator bookkeeping (e.g. the body-part
98
+ # group of a region), not a character
92
99
  typ = chars.get(ch, {}).get("type", "categorical")
93
100
  rows.append({"specimen_id": pol.specimen_id(stem, sp, profile), "species": sp, "category": cat,
94
101
  "structure": cat, "character": ch, "type": "count" if typ == "count" else "nominal",
95
102
  "state": str(st).strip(), "image": stem, "source": "human"})
103
+ # v1.1: presence / absence, for every structure recorded absent somewhere (a recorded character loss)
104
+ from descriptron_check_completeness_v1 import structure_states
105
+ states, _, _ = structure_states([json.load(open(p)) for p in coco_paths], aliases)
106
+ lost = {ch for rec in states.values() for ch, v in rec.items() if v == "absent"}
107
+ for stem_, rec in states.items():
108
+ sp = species_of.get(stem_) or species_of.get(stem_.replace(" ", "_"))
109
+ if sp is None:
110
+ continue
111
+ for ch in lost:
112
+ if rec.get(ch) in ("present", "absent"):
113
+ cat = ch.replace(" ", "_")
114
+ rows.append({"specimen_id": pol.specimen_id(stem_, sp, profile), "species": sp, "category": cat,
115
+ "structure": cat, "character": "presence", "type": "nominal", "state": rec[ch],
116
+ "image": stem_, "source": "human"})
96
117
  df = pd.DataFrame(rows, columns=["specimen_id", "species", "category", "structure", "character", "type",
97
118
  "state", "image", "source"])
98
119
  # a character the vocabulary does not know (added by the user, or no vocabulary found) is a count when every
@@ -162,11 +183,13 @@ def to_features(per, tier, min_species, known, labels=None):
162
183
  if g["state"].nunique() < 2:
163
184
  dropped["same state in every specimen"].append(name); continue
164
185
  lab = labels.get(ch, ch.replace("_", " "))
186
+ presence = ch == "presence" # v1.1: "eye = absent", worded "eye absent"
165
187
  if typ == "count":
166
188
  feats = [(f"{cat}.coded_{slug(ch)}", f"{cat}: {lab}", "meristic", "count", "",
167
189
  g.set_index("specimen_id")["state"].astype(float))]
168
190
  else:
169
- feats = [(f"{cat}.coded_{slug(ch)}__{slug(s)}", f"{cat}: {lab} = {s}", "coded_state", "", s,
191
+ feats = [(f"{cat}.coded_{slug(ch)}__{slug(s)}", f"{cat} = {s}" if presence else f"{cat}: {lab} = {s}",
192
+ "coded_state", "", s,
170
193
  (g.set_index("specimen_id")["state"] == s).astype(float)) for s in sorted(g["state"].unique())]
171
194
  meta = g.drop_duplicates("specimen_id").set_index("specimen_id")
172
195
  for fid, label, fam, unit, state, vals in feats:
@@ -226,11 +249,24 @@ def main():
226
249
  clash = set(add_fd["feature_id"]) & set(fd["feature_id"])
227
250
  if clash:
228
251
  sys.exit(f"feature ids already in the matrix: {sorted(clash)[:5]}")
229
- pd.concat([fd, add_fd], ignore_index=True).to_csv(out / "feature_dictionary.tsv", sep="\t", index=False)
230
- pd.concat([long, add_long], ignore_index=True).to_csv(out / "specimen_matrix_long.csv", index=False)
231
- for extra in ("outlier_flags.tsv", "filter_report_v2.json"):
232
- if (src / extra).exists():
233
- shutil.copy2(src / extra, out / extra)
252
+ fd_all = pd.concat([fd, add_fd], ignore_index=True)
253
+ long_all = pd.concat([long, add_long], ignore_index=True)
254
+ fd_all.to_csv(out / "feature_dictionary.tsv", sep="\t", index=False)
255
+ long_all.to_csv(out / "specimen_matrix_long.csv", index=False)
256
+ # 1.1: every other file of the matrix folder comes along (the description and audit steps read the species
257
+ # summary, the full-features table and the diagnostic report); the summaries are rebuilt to include the
258
+ # added characters, exactly as biorag_key_feature_filter_v2 writes them
259
+ rebuilt = {"feature_dictionary.tsv", "specimen_matrix_long.csv", "species_feature_summary.csv",
260
+ "coverage_species_by_structure.tsv"}
261
+ for f in src.iterdir():
262
+ if f.is_file() and f.name not in rebuilt and not (out / f.name).exists():
263
+ shutil.copy2(f, out / f.name)
264
+ ss = (long_all.groupby(["species", "feature_id"])["value"]
265
+ .agg(n="count", min="min", max="max", mean="mean", sd="std", median="median").reset_index())
266
+ ss = ss.merge(fd_all[["feature_id", "tier", "unit", "category", "column"]], on="feature_id", how="left")
267
+ ss.to_csv(out / "species_feature_summary.csv", index=False)
268
+ (long_all.groupby(["species", "category"])["specimen_id"].nunique().unstack(fill_value=0)
269
+ .to_csv(out / "coverage_species_by_structure.tsv", sep="\t"))
234
270
  n_key = lambda f: int((f["tier"] == "key").sum()) if len(f) else 0 # noqa: E731
235
271
  print(f" matrix: {len(add_fd)} features added ({n_key(add_fd)} at tier key); key features "
236
272
  f"{n_key(fd)} -> {n_key(fd) + n_key(add_fd)} -> {out}")
@@ -1279,7 +1279,8 @@ def verify_coded_states(field: str, text: str, code: str, ev: Evidence, issues:
1279
1279
  terms = {info.get("term", ""), info.get("short", ""), cat.replace("_", " ")}
1280
1280
  if not any(t and re.search(rf"\b{re.escape(t.lower())}\b", low) for t in terms):
1281
1281
  continue
1282
- names_char = bool(re.search(rf"\b{re.escape(r['label'].lower())}\b", low)) or \
1282
+ names_char = ch == "presence" or \
1283
+ bool(re.search(rf"\b{re.escape(r['label'].lower())}\b", low)) or \
1283
1284
  bool(re.search(rf"\b{re.escape(ch.replace('_', ' ').lower())}\b", low))
1284
1285
  for st in sorted(vocab[(cat, ch)], key=len, reverse=True):
1285
1286
  for m in re.finditer(rf"\b{re.escape(st.lower())}\b", low):
@@ -353,7 +353,8 @@ def coded_lines(ev: Evidence, code: str):
353
353
  lines.append(f" {info['term']}, {r['label']}: {own}" + (f" | other species: {oth}" if oth else ""))
354
354
  for st, _ in states:
355
355
  if n_other and st not in others:
356
- diag.append(f"{info['term']} with {r['label']} {st}: recorded in no other species "
356
+ diag.append((f"{info['term']} {st}" if ch == "presence" else f"{info['term']} with {r['label']} {st}")
357
+ + f": recorded in no other species "
357
358
  f"(of {n_other} with this character recorded)")
358
359
  nums.append(n_other)
359
360
  if diag:
@@ -547,6 +547,7 @@ def template_lead(chars: List[Dict], side: str) -> str:
547
547
  # a state the taxonomist recorded ("elytron: setae = dense"): "setae dense" / "setae not dense"
548
548
  base, _, st = lab.rpartition(" = ")
549
549
  word = st if c[f"{side}_operator"] == ">" else f"not {st}"
550
+ word = {"not absent": "present", "not present": "absent"}.get(word, word) # presence of a structure
550
551
  parts.append(f"{pre}{base} {usually}{word} (n = {c[f'{side}_n']})")
551
552
  continue
552
553
  state = "present" if c[f"{side}_operator"] == ">" else "absent"
@@ -412,6 +412,11 @@ def main():
412
412
  help="CSV of annotations to drop (image_filename, category_name, reason)")
413
413
  ap.add_argument("--exclude_flagged", action="store_true",
414
414
  help="Drop rows flagged by the outlier/label-swap screen (see outlier_flags.tsv)")
415
+ ap.add_argument("--completeness_table", default=None,
416
+ help="completeness_by_character.tsv from descriptron_check_completeness_v1 (with --min_completeness)")
417
+ ap.add_argument("--min_completeness", type=float, default=0.0,
418
+ help="drop every feature of a structure scored (present or recorded absent) in fewer than this "
419
+ "share of specimens, e.g. 0.8; needs --completeness_table (default 0 = keep all)")
415
420
  ap.add_argument("--cache_dir", default=None,
416
421
  help="Optional BioRAG cache to copy with computational sentences removed (v1 filter)")
417
422
  args = ap.parse_args()
@@ -468,6 +473,18 @@ def main():
468
473
  long = long.rename(columns={"group_label": "species"})
469
474
  long = long[["species", "specimen_id", "sex", "category", "base_category", "column",
470
475
  "feature_id", "tier", "family", "value", "n_images"]]
476
+ if args.min_completeness > 0: # 2.7.8: drop structures scored in too few specimens
477
+ if not args.completeness_table:
478
+ sys.exit("--min_completeness needs --completeness_table (descriptron_check_completeness_v1)")
479
+ ct = pd.read_csv(args.completeness_table, sep="\t")
480
+ ct = ct[~ct["character"].str.contains(" ")] # structures (masks); keypoint slots are 'name keypoint N'
481
+ low = set(ct.loc[ct["share_scored"] < args.min_completeness, "character"])
482
+ dropped = sorted(set(long["base_category"]) & low)
483
+ long = long[~long["base_category"].isin(low)]
484
+ report["completeness"] = {"table": str(args.completeness_table), "min_completeness": args.min_completeness,
485
+ "structures_dropped": dropped}
486
+ print(f"Completeness: {len(dropped)} structure(s) scored in < {args.min_completeness:.0%} of specimens "
487
+ f"dropped: {dropped[:8]}")
471
488
  long.to_csv(dst / "specimen_matrix_long.csv", index=False)
472
489
 
473
490
  # ---- feature dictionary
@@ -515,6 +515,9 @@ def load_landmark_gpa(gpa_dirs: List[str]) -> Dict[Tuple[str, str], Dict]:
515
515
  key=lambda c: int(c.replace("lm", "").replace("_x", "")))
516
516
  lm_y_cols = [c.replace("_x", "_y") for c in lm_x_cols]
517
517
  n_lm = len(lm_x_cols)
518
+ # 2.7.8: landmark numbers from the column names (a subset analysed by --landmarks_present_in_all
519
+ # is lm1, lm2, lm5, ...; position would renumber them)
520
+ lm_ids = [int(c.replace("lm", "").replace("_x", "")) for c in lm_x_cols]
518
521
 
519
522
  for _, row in df_proc.iterrows():
520
523
  raw_fn = str(row[fn_col]).strip()
@@ -536,7 +539,7 @@ def load_landmark_gpa(gpa_dirs: List[str]) -> Dict[Tuple[str, str], Dict]:
536
539
  dx = coords[i][0] - coords[j][0]
537
540
  dy = coords[i][1] - coords[j][1]
538
541
  dist = (dx * dx + dy * dy) ** 0.5
539
- data[key][f"lmk_dist_{i+1}_{j+1}"] = dist
542
+ data[key][f"lmk_dist_{lm_ids[i]}_{lm_ids[j]}"] = dist # real numbers
540
543
 
541
544
  logger.info(f"Landmark GPA: {len(data)} entries")
542
545
  return data
@@ -0,0 +1,375 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ descriptron_check_completeness_v1.py - which structures and keypoints each specimen has, lacks, or was never scored for
4
+ =====================================================================================================================
5
+
6
+ A structure with no mask can mean three different things, and only the taxonomist can say which:
7
+
8
+ present a mask of the structure, or a visible keypoint (v > 0);
9
+ absent recorded as lost in the GUI (Record absent -> Absent, or a Lost keypoint): the specimen does not have
10
+ it, e.g. no eyes in a subterranean weevil. This is a CHARACTER STATE and enters the matrix, key and
11
+ descriptions as 'absent';
12
+ unknown recorded as not visible (hidden, broken, out of focus) or a negative keypoint: MISSING DATA;
13
+ gap nothing recorded at all: not annotated, skipped or not predicted. Also missing data, but nobody has
14
+ looked - so every gap is listed here for checking.
15
+
16
+ Only an explicit 'absent' is a character loss. A gap never becomes one by itself.
17
+
18
+ Reads the COCO files written by the Descriptron GUI (v84+: images[].structure_status and
19
+ annotations[].keypoint_status; older files simply have no records, so every missing structure is a gap).
20
+
21
+ python descriptron_check_completeness_v1.py --coco annotations.json [more.json ...] \\
22
+ [--group_labels group_labels.csv | --metadata specimens.csv | --species_regex '^([^_]+_[^_]+)'] \\
23
+ [--taxon_profile profile.yaml | --specimen_regex '(sp\\d+_\\d+)'] \\
24
+ [--structures eye antenna ...] [--strict] --out_dir completeness/
25
+
26
+ The unit is the SPECIMEN: images of one specimen (head, leg, wing photographed separately) are merged, using the
27
+ taxon profile's specimen pattern (as the matrix does), --specimen_regex, or the Darwin Core occurrenceID of
28
+ --metadata; without any of these each image counts as a specimen. A structure is present if any image of the
29
+ specimen shows it, absent if recorded absent and shown in none. With --taxon_profile, structures limited to one sex
30
+ (profile 'sex: male' / 'female') are not expected in specimens of the other sex.
31
+
32
+ Writes
33
+ completeness_matrix.tsv one row per specimen x character (structure, or structure keypoint N): state
34
+ completeness_worklist.csv every gap, most likely omissions first: missing in this specimen while
35
+ conspecifics have it (probably forgotten), then missing in the whole species
36
+ (possibly a real loss - check and record it), then specimens with no species
37
+ completeness_conflicts.csv recorded absent but a mask / visible keypoint exists (or the reverse)
38
+ completeness_by_character.tsv per character: present / absent / unknown / gap and the share scored
39
+ (present + absent); use it to drop poorly scored characters (--min_completeness in
40
+ biorag_key_feature_filter_v2)
41
+ completeness_heatmap.png specimens x characters coloured by state
42
+ completeness_summary.json
43
+ --strict exits with status 1 while any gap or conflict remains (for pipelines that should stop).
44
+ """
45
+ import argparse
46
+ import json
47
+ import os
48
+ import re
49
+ import sys
50
+ from collections import Counter, defaultdict
51
+ from pathlib import Path
52
+
53
+ VERSION = "1.0"
54
+ IMAGE_EXTS = (".tif", ".tiff", ".jpg", ".jpeg", ".png", ".bmp", ".webp")
55
+ NOT_STRUCTURES = {"Trash", "Custom", "trash", "custom"}
56
+ STATES = ("present", "absent", "unknown", "gap")
57
+
58
+
59
+ def stem(name):
60
+ b = os.path.basename(str(name))
61
+ return b[: -len(os.path.splitext(b)[1])] if os.path.splitext(b)[1].lower() in IMAGE_EXTS else b
62
+
63
+
64
+ def _has_geometry(a):
65
+ seg = a.get("segmentation")
66
+ if isinstance(seg, dict):
67
+ return bool(seg.get("counts"))
68
+ if isinstance(seg, list):
69
+ return any(isinstance(p, list) and len(p) >= 6 for p in seg)
70
+ return False
71
+
72
+
73
+ def keypoint_names(cat, n):
74
+ names = cat.get("keypoints") if isinstance(cat.get("keypoints"), list) else []
75
+ return [str(names[i]) if i < len(names) and names[i] else f"keypoint {i + 1}" for i in range(n)]
76
+
77
+
78
+ def structure_states(coco_dicts, aliases=None):
79
+ """{image stem: {character: state}} plus the character list, from one or more COCO dicts.
80
+ Characters are structure names (masks) and '<structure> <keypoint name>' (keypoint slots). Images with
81
+ several COCO files are merged: present beats absent beats unknown (a conflict is reported separately)."""
82
+ aliases = aliases or {}
83
+ st = defaultdict(dict) # stem -> char -> set of evidence
84
+ chars_mask, chars_kp = set(), {}
85
+ images = set()
86
+ conflicts = []
87
+ for d in coco_dicts:
88
+ cats = {c["id"]: c for c in d.get("categories", [])}
89
+ imgs = {im["id"]: im for im in d.get("images", [])}
90
+ for im in d.get("images", []):
91
+ s = stem(im.get("file_name") or im.get("id"))
92
+ images.add(s)
93
+ for name, val in (im.get("structure_status") or {}).items():
94
+ name = aliases.get(name, name)
95
+ if name in NOT_STRUCTURES:
96
+ continue
97
+ chars_mask.add(name)
98
+ st[s].setdefault(name, set()).add("absent" if val == "absent" else "unknown")
99
+ for a in d.get("annotations", []):
100
+ im = imgs.get(a.get("image_id"), {})
101
+ s = stem(im.get("file_name") or a.get("image_id"))
102
+ images.add(s)
103
+ cat = cats.get(a.get("category_id"), {"name": str(a.get("category_id"))})
104
+ cname = aliases.get(cat.get("name"), cat.get("name"))
105
+ if cname in NOT_STRUCTURES or a.get("source_line_category"):
106
+ continue
107
+ if a.get("keypoints"):
108
+ k = a["keypoints"]; n = len(k) // 3
109
+ names = keypoint_names(cat, n)
110
+ kst = a.get("keypoint_status") or []
111
+ chars_kp.setdefault(cname, names if len(names) >= len(chars_kp.get(cname, [])) else chars_kp[cname])
112
+ for i in range(n):
113
+ v = int(k[3 * i + 2])
114
+ state = "present" if v > 0 else ("absent" if len(kst) > i and kst[i] == "absent" else "unknown")
115
+ st[s].setdefault(f"{cname} {names[i]}", set()).add(state)
116
+ elif _has_geometry(a) and not a.get("is_line"):
117
+ chars_mask.add(cname)
118
+ st[s].setdefault(cname, set()).add("present")
119
+ out = {}
120
+ for s in sorted(images):
121
+ rec = {}
122
+ for ch, ev in st.get(s, {}).items():
123
+ if "present" in ev and "absent" in ev:
124
+ conflicts.append({"image": s, "character": ch,
125
+ "problem": "recorded absent but a mask / visible keypoint exists"})
126
+ rec[ch] = "present" if "present" in ev else ("absent" if "absent" in ev else "unknown")
127
+ out[s] = rec
128
+ chars = sorted(chars_mask) + [f"{c} {n}" for c in sorted(chars_kp) for n in chars_kp[c]]
129
+ return out, chars, conflicts
130
+
131
+
132
+ def specimen_map(images, species, profile=None, specimen_regex=None, metadata=None, media_col="associatedMedia",
133
+ id_col="occurrenceID"):
134
+ """image stem -> (specimen key, sex or None)"""
135
+ occ = {}
136
+ if metadata:
137
+ import csv
138
+ with open(metadata, newline="") as f:
139
+ for row in csv.DictReader(f):
140
+ for m in str(row.get(media_col, "")).split("|"):
141
+ if m.strip() and row.get(id_col):
142
+ occ[stem(m.strip())] = row[id_col]
143
+ rx = re.compile(specimen_regex) if specimen_regex else None
144
+ pol = None
145
+ if profile is not None:
146
+ import biorag_feature_policy as pol
147
+ out = {}
148
+ for s in images:
149
+ sp = species.get(s) or "?"
150
+ sex = pol.specimen_sex(s, profile) if pol else None
151
+ if s in occ:
152
+ key = occ[s]
153
+ elif rx is not None and rx.search(s):
154
+ m = rx.search(s); key = f"{sp}|{m.group(1) if m.groups() else m.group(0)}"
155
+ elif pol is not None:
156
+ key = pol.specimen_id(s, sp, profile)
157
+ else:
158
+ key = s
159
+ out[s] = (key, None if sex in (None, "unknown") else sex)
160
+ return out
161
+
162
+
163
+ def merge_specimens(states, spec):
164
+ """per-image states -> per-specimen states (present > absent > unknown); conflicts across images reported"""
165
+ merged, images_of, conflicts = defaultdict(lambda: defaultdict(set)), defaultdict(list), []
166
+ for s, rec in states.items():
167
+ key = spec[s][0]
168
+ images_of[key].append(s)
169
+ for ch, v in rec.items():
170
+ merged[key][ch].add(v)
171
+ out = {}
172
+ for key, rec in merged.items():
173
+ out[key] = {}
174
+ for ch, ev in rec.items():
175
+ if "present" in ev and "absent" in ev:
176
+ conflicts.append({"image": key, "character": ch,
177
+ "problem": "absent on one image of the specimen, present on another"})
178
+ out[key][ch] = "present" if "present" in ev else ("absent" if "absent" in ev else "unknown")
179
+ for key in images_of:
180
+ out.setdefault(key, {})
181
+ return out, images_of, conflicts
182
+
183
+
184
+ def species_map(images, group_labels=None, metadata=None, species_regex=None, media_col="associatedMedia",
185
+ species_col="scientificName"):
186
+ sp = {}
187
+ if group_labels:
188
+ import csv
189
+ with open(group_labels, newline="") as f:
190
+ r = csv.DictReader(f)
191
+ fn = "filename" if "filename" in r.fieldnames else r.fieldnames[0]
192
+ for row in r:
193
+ sp[stem(row[fn])] = row.get("group_label") or row.get("species")
194
+ if metadata:
195
+ import csv
196
+ with open(metadata, newline="") as f:
197
+ for row in csv.DictReader(f):
198
+ for m in str(row.get(media_col, "")).split("|"):
199
+ if m.strip():
200
+ sp[stem(m.strip())] = row.get(species_col)
201
+ if species_regex:
202
+ rx = re.compile(species_regex)
203
+ for s in images:
204
+ if s not in sp:
205
+ m = rx.search(s)
206
+ if m:
207
+ sp[s] = m.group(1) if m.groups() else m.group(0)
208
+ return {s: sp.get(s) for s in images}
209
+
210
+
211
+ def check(states, chars, species, structures=None, sex_of=None, structure_sex=None):
212
+ """states/species keyed by specimen (or image). sex_of: specimen -> 'male'/'female'/None; structure_sex:
213
+ structure -> 'male'/'female' for structures limited to one sex (not expected in the other)"""
214
+ if structures:
215
+ keep = set(structures)
216
+ chars = [c for c in chars if c in keep or c.split(" ")[0] in keep]
217
+ sex_of, structure_sex = sex_of or {}, structure_sex or {}
218
+ rows = []
219
+ for s, rec in states.items():
220
+ for ch in chars:
221
+ need = structure_sex.get(ch) or structure_sex.get(ch.split(" ")[0])
222
+ if need and sex_of.get(s) and sex_of[s] != need and ch not in rec:
223
+ continue # e.g. an aedeagus is not expected in a female
224
+ rows.append({"image": s, "species": species.get(s) or "", "character": ch, "state": rec.get(ch, "gap")})
225
+ # worklist: for each gap, what the conspecifics show
226
+ by_sp_char = defaultdict(Counter)
227
+ for r in rows:
228
+ by_sp_char[(r["species"], r["character"])][r["state"]] += 1
229
+ work, seen_species_gap = [], {}
230
+ for r in rows:
231
+ if r["state"] != "gap":
232
+ continue
233
+ c = by_sp_char[(r["species"], r["character"])]
234
+ n_other = sum(c.values()) - 1
235
+ if not r["species"]:
236
+ pri, why = 3, "no species given: check this specimen"
237
+ elif c["present"] > 0:
238
+ pri, why = 1, (f"probably not annotated: {c['present']} of {n_other} other specimen(s) of the species "
239
+ "have it - annotate it, or record it as absent / not visible")
240
+ elif c["absent"] > 0:
241
+ pri, why = 2, (f"recorded absent in {c['absent']} other specimen(s) of the species - if this one lacks "
242
+ "it too, record it as absent")
243
+ else:
244
+ # nothing recorded in any specimen of the species: one row for the species (often a body part that
245
+ # was not imaged for it, or a real loss that has not been recorded yet)
246
+ key = ("__species__", r["species"], r["character"])
247
+ if key in seen_species_gap:
248
+ seen_species_gap[key]["image"] += f"; {r['image']}"
249
+ continue
250
+ pri, why = 2, ("nothing recorded in any specimen of the species: if the species lacks it, record it as "
251
+ "absent; if the part was not imaged, ignore (or record not visible)")
252
+ seen_species_gap[key] = {"priority": pri, "image": r["image"], "species": r["species"],
253
+ "character": r["character"], "reason": why}
254
+ work.append(seen_species_gap[key])
255
+ continue
256
+ work.append({"priority": pri, "image": r["image"], "species": r["species"], "character": r["character"],
257
+ "reason": why})
258
+ work.sort(key=lambda w: (w["priority"], w["species"], w["image"], w["character"]))
259
+ per = []
260
+ for ch in chars:
261
+ c = Counter(r["state"] for r in rows if r["character"] == ch)
262
+ n = sum(c.values())
263
+ per.append({"character": ch, **{k: c.get(k, 0) for k in STATES}, "n_specimens": n,
264
+ "share_scored": round((c.get("present", 0) + c.get("absent", 0)) / n, 4) if n else 0.0,
265
+ "species_with_absence": len({r["species"] for r in rows
266
+ if r["character"] == ch and r["state"] == "absent"})})
267
+ return rows, work, per, chars
268
+
269
+
270
+ def heatmap(rows, chars, images, path):
271
+ import matplotlib
272
+ matplotlib.use("Agg")
273
+ import matplotlib.pyplot as plt
274
+ from matplotlib.colors import ListedColormap
275
+ import numpy as np
276
+ code = {"present": 0, "absent": 1, "unknown": 2, "gap": 3}
277
+ idx = {s: i for i, s in enumerate(images)}; jdx = {c: j for j, c in enumerate(chars)}
278
+ M = np.full((len(images), len(chars)), 3)
279
+ for r in rows:
280
+ M[idx[r["image"]], jdx[r["character"]]] = code[r["state"]]
281
+ h = min(0.18 * len(images) + 2, 40); w = min(0.22 * len(chars) + 3, 40)
282
+ fig, ax = plt.subplots(figsize=(w, h))
283
+ ax.imshow(M, aspect="auto", interpolation="nearest",
284
+ cmap=ListedColormap(["#4caf50", "#9b30ff", "#b0b0b0", "#ffffff"]), vmin=-0.5, vmax=3.5)
285
+ ax.set_xticks(range(len(chars))); ax.set_xticklabels(chars, rotation=90, fontsize=6)
286
+ ax.set_yticks(range(len(images))); ax.set_yticklabels(images, fontsize=5)
287
+ ax.set_xticks(np.arange(-0.5, len(chars)), minor=True); ax.set_yticks(np.arange(-0.5, len(images)), minor=True)
288
+ ax.grid(which="minor", color="#dddddd", lw=0.3); ax.tick_params(which="minor", length=0)
289
+ from matplotlib.patches import Patch
290
+ ax.legend(handles=[Patch(color="#4caf50", label="present"), Patch(color="#9b30ff", label="absent (lost)"),
291
+ Patch(color="#b0b0b0", label="not visible (missing data)"),
292
+ Patch(facecolor="#ffffff", edgecolor="#999", label="nothing recorded (check)")],
293
+ loc="upper left", bbox_to_anchor=(1.01, 1), fontsize=7, frameon=False)
294
+ ax.set_title("Completeness: what each specimen has, lacks, or was never scored for", fontsize=9)
295
+ fig.tight_layout(); fig.savefig(path, dpi=150); plt.close(fig)
296
+
297
+
298
+ def main():
299
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
300
+ ap.add_argument("--coco", nargs="+", required=True)
301
+ ap.add_argument("--group_labels", default=None, help="filename,group_label CSV (the pipeline's species table)")
302
+ ap.add_argument("--metadata", default=None, help="Darwin Core specimen table (associatedMedia, scientificName)")
303
+ ap.add_argument("--species_regex", default=None, help="regex on the image name; group 1 = species")
304
+ ap.add_argument("--taxon_profile", default=None,
305
+ help="taxon profile YAML: its specimen pattern merges the images of one specimen, and its "
306
+ "sex-limited structures are not expected in the other sex")
307
+ ap.add_argument("--specimen_regex", default=None,
308
+ help="regex on the image name whose group 1 identifies the specimen (images of one specimen merged)")
309
+ ap.add_argument("--structures", nargs="*", default=None, help="check only these structures (default: all)")
310
+ ap.add_argument("--aliases", default=None, help="JSON {old name: new name} for category names")
311
+ ap.add_argument("--strict", action="store_true", help="exit 1 while any gap or conflict remains")
312
+ ap.add_argument("--no_figure", action="store_true")
313
+ ap.add_argument("--out_dir", required=True)
314
+ a = ap.parse_args()
315
+ out = Path(a.out_dir); out.mkdir(parents=True, exist_ok=True)
316
+ aliases = json.load(open(a.aliases)) if a.aliases else {}
317
+ dicts = [json.load(open(p)) for p in a.coco]
318
+ profile, structure_sex = None, {}
319
+ if a.taxon_profile:
320
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
321
+ import biorag_feature_policy as pol
322
+ profile = pol.load_taxon_profile(a.taxon_profile)
323
+ aliases = {**dict(profile.get("category_aliases") or {}), **aliases}
324
+ states, chars, conflicts = structure_states(dicts, aliases)
325
+ species = species_map(list(states), a.group_labels, a.metadata, a.species_regex)
326
+ if profile is not None:
327
+ for ch in chars:
328
+ sx = pol.structure_info(ch.split(" ")[0], profile).get("sex")
329
+ if sx in ("male", "female"):
330
+ structure_sex[ch.split(" ")[0]] = sx
331
+ spec = specimen_map(list(states), species, profile, a.specimen_regex, a.metadata)
332
+ states, images_of, c2 = merge_specimens(states, spec)
333
+ conflicts += c2
334
+ sex_of = {}
335
+ for s, (key, sx) in spec.items():
336
+ if sx:
337
+ sex_of[key] = sx
338
+ species = {spec[s][0]: species.get(s) for s in spec}
339
+ rows, work, per, chars = check(states, chars, species, a.structures, sex_of, structure_sex)
340
+ for w in work: # which images to open to fix it
341
+ w["images"] = "; ".join(images_of.get(w["image"], [])[:6])
342
+ import csv
343
+
344
+ def write(path, recs, cols, sep=","):
345
+ with open(path, "w", newline="") as f:
346
+ w = csv.DictWriter(f, fieldnames=cols, delimiter=sep); w.writeheader(); w.writerows(recs)
347
+ write(out / "completeness_matrix.tsv", rows, ["image", "species", "character", "state"], "\t")
348
+ write(out / "completeness_worklist.csv", work, ["priority", "image", "species", "character", "reason", "images"])
349
+ write(out / "completeness_conflicts.csv", conflicts, ["image", "character", "problem"])
350
+ write(out / "completeness_by_character.tsv", per, ["character", *STATES, "n_specimens", "share_scored",
351
+ "species_with_absence"], "\t")
352
+ if not a.no_figure and rows:
353
+ heatmap(rows, chars, list(states), out / "completeness_heatmap.png")
354
+ tot = Counter(r["state"] for r in rows)
355
+ summ = {"version": VERSION, "coco": [str(p) for p in a.coco], "specimens": len(states), "images": len(spec),
356
+ "characters": len(chars),
357
+ "states": dict(tot), "gaps": tot.get("gap", 0), "worklist_rows": len(work),
358
+ "gaps_priority_1": sum(w["priority"] == 1 for w in work),
359
+ "conflicts": len(conflicts), "no_species": sum(1 for s in states if not species.get(s))}
360
+ json.dump(summ, open(out / "completeness_summary.json", "w"), indent=1)
361
+ print(f"{len(states)} specimens ({len(spec)} images) x {len(chars)} characters: present {tot.get('present', 0)}, absent (lost) "
362
+ f"{tot.get('absent', 0)}, not visible {tot.get('unknown', 0)}, nothing recorded {tot.get('gap', 0)}")
363
+ if work:
364
+ print(f" worklist: {summ['gaps_priority_1']} gap(s) probably not annotated (conspecifics have the structure), "
365
+ f"{sum(w['priority'] == 2 for w in work)} species x structure with nothing recorded in any specimen "
366
+ f"-> {out / 'completeness_worklist.csv'}")
367
+ if conflicts:
368
+ print(f" {len(conflicts)} conflict(s): recorded absent but annotated -> {out / 'completeness_conflicts.csv'}")
369
+ print(f"-> {out}")
370
+ if a.strict and (work or conflicts):
371
+ sys.exit(1)
372
+
373
+
374
+ if __name__ == "__main__":
375
+ main()