descriptron-core 2.7.0__tar.gz → 2.7.6__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 (149) hide show
  1. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/PKG-INFO +1 -1
  2. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/pyproject.toml +1 -1
  3. descriptron_core-2.7.6/src/descriptron_core/tools/biorag_coded_states_from_coco_v1.py +245 -0
  4. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_confabulation_checker_v2.py +54 -0
  5. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_description_refiner_v1.py +67 -1
  6. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_feature_policy.py +2 -0
  7. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_key_builder_v1.py +11 -2
  8. descriptron_core-2.7.6/src/descriptron_core/tools/descriptron_community_v1.py +866 -0
  9. descriptron_core-2.7.6/src/descriptron_core/tools/descriptron_descriptive_characters.json +1270 -0
  10. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/run_full_pipeline_v2.py +18 -1
  11. descriptron_core-2.7.6/src/descriptron_core/tools/validate_community_vs_r.py +107 -0
  12. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/.gitignore +0 -0
  13. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/LICENSE +0 -0
  14. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/NOTICE +0 -0
  15. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/README.md +0 -0
  16. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/__init__.py +0 -0
  17. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/cli.py +0 -0
  18. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/data/biorag_prompts/biorag_system_prompts_v1.txt +0 -0
  19. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/data/biorag_prompts/biorag_system_prompts_v2.txt +0 -0
  20. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/data/biorag_prompts/diaphorina_user_prompts.txt +0 -0
  21. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/data/biorag_prompts/ontologies/aism.obo +0 -0
  22. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/data/biorag_prompts/ontologies/pato.obo +0 -0
  23. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/SDD.xsd +0 -0
  24. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/SDD_EnumLib.xsd +0 -0
  25. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/SDD_TypeLib.xsd +0 -0
  26. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_BasePattern.xsd +0 -0
  27. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_CoreExtensions.xsd +0 -0
  28. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_CoreOntology.xsd +0 -0
  29. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_EnumLib.xsd +0 -0
  30. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_LinneanCore.xsd +0 -0
  31. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_TypeLib.xsd +0 -0
  32. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/w3c-schemata/xml.xsd +0 -0
  33. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/data/biorag_prompts/schemas/taxpub/sample_treatment_deposit_example.xml +0 -0
  34. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/data/biorag_prompts/schemas/taxpub/sample_zookeys_26056_tp.xml +0 -0
  35. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/data/biorag_prompts/schemas/taxpub/tax-treatment-NS0-v1_flat.dtd +0 -0
  36. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/data/biorag_prompts/taxon_profiles/TEMPLATE_taxon_profile.yaml +0 -0
  37. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/data/biorag_prompts/taxon_profiles/diaphorina_taxon_profile.yaml +0 -0
  38. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/FHS_and_CLAHE_V21.py +0 -0
  39. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_add_discrete_characters_v1.py +0 -0
  40. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_annotation_reorient_v1.py +0 -0
  41. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_annotation_screen_v1.py +0 -0
  42. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_autapomorphy_v1.py +0 -0
  43. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_calibrate_v1.py +0 -0
  44. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_calibration_figures_v1.py +0 -0
  45. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_character_gate_v1.py +0 -0
  46. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_character_reliability_v1.py +0 -0
  47. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_character_robustness_v1.py +0 -0
  48. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_collaborator_forms_v1.py +0 -0
  49. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_computed_sets_v1.py +0 -0
  50. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_congruence_compare_v1.py +0 -0
  51. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_descriptive_scoring_v1.py +0 -0
  52. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_frame_orientation_calibrate_v1.py +0 -0
  53. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_frame_orientation_v1.py +0 -0
  54. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_gate_compare_v1.py +0 -0
  55. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_graph_identify_v1.py +0 -0
  56. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_graph_support_figure_v1.py +0 -0
  57. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_homology_frame_v1.py +0 -0
  58. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_instrument_compare_v3.py +0 -0
  59. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_key_feature_filter_v2.py +0 -0
  60. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_key_fuzzy_figures_v1.py +0 -0
  61. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_key_fuzzy_v1.py +0 -0
  62. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_key_holdout_v1.py +0 -0
  63. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_key_jackknife_support_v1.py +0 -0
  64. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_label_conflict_check_v1.py +0 -0
  65. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_llm_backend.py +0 -0
  66. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_make_taxon_profile_v1.py +0 -0
  67. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_methods_summary_figure_v1.py +0 -0
  68. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_novelty_score_v1.py +0 -0
  69. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_ontology_annotator_v2.py +0 -0
  70. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_outgroup_batch_check_v1.py +0 -0
  71. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_outline_shape_v1.py +0 -0
  72. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_prompts/biorag_system_prompts_v1.txt +0 -0
  73. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_prompts/biorag_system_prompts_v2.txt +0 -0
  74. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_prompts/diaphorina_user_prompts.txt +0 -0
  75. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_prompts/ontologies/aism.obo +0 -0
  76. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_prompts/ontologies/pato.obo +0 -0
  77. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/SDD.xsd +0 -0
  78. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/SDD_EnumLib.xsd +0 -0
  79. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/SDD_TypeLib.xsd +0 -0
  80. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_BasePattern.xsd +0 -0
  81. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_CoreExtensions.xsd +0 -0
  82. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_CoreOntology.xsd +0 -0
  83. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_EnumLib.xsd +0 -0
  84. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_LinneanCore.xsd +0 -0
  85. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_TypeLib.xsd +0 -0
  86. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/w3c-schemata/xml.xsd +0 -0
  87. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_prompts/schemas/taxpub/sample_treatment_deposit_example.xml +0 -0
  88. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_prompts/schemas/taxpub/sample_zookeys_26056_tp.xml +0 -0
  89. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_prompts/schemas/taxpub/tax-treatment-NS0-v1_flat.dtd +0 -0
  90. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_prompts/taxon_profiles/TEMPLATE_taxon_profile.yaml +0 -0
  91. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_prompts/taxon_profiles/diaphorina_taxon_profile.yaml +0 -0
  92. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_provenance_v1.py +0 -0
  93. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_rebuild_downstream_v1.sh +0 -0
  94. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_reliability_figures_v1.py +0 -0
  95. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_rename_species_v1.py +0 -0
  96. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_rerun_descriptive_tables_v1.sh +0 -0
  97. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_retest_compare_v1.py +0 -0
  98. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_specimen_id.py +0 -0
  99. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_subjective_checks_v1.py +0 -0
  100. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_type_material_v1.py +0 -0
  101. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_vlm_characters_v1.py +0 -0
  102. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_vlm_heatmap_figures_v1.py +0 -0
  103. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_vlm_reliability_figure_v1.py +0 -0
  104. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biorag_vlm_roi_figures_v1.py +0 -0
  105. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biosyslit_rag_retrieval.py +0 -0
  106. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/biosyslit_rag_retrieval_v2.py +0 -0
  107. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/build_monograph_exports_v1.py +0 -0
  108. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/build_species_treatment_docx_v2.py +0 -0
  109. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/coco_combiner_V13.py +0 -0
  110. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/coco_converter_v24.py +0 -0
  111. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/coi_species_tree_v1.py +0 -0
  112. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/color_extraction_with_names-v8.py +0 -0
  113. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/color_phenomics_homology_v2_1.py +0 -0
  114. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/compile_specimen_data.py +0 -0
  115. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/descriptron_centerline.py +0 -0
  116. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/descriptron_character_signal_v1.py +0 -0
  117. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/descriptron_check_cross_image_copies_v1.py +0 -0
  118. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/descriptron_coco_tools.py +0 -0
  119. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/descriptron_convert.py +0 -0
  120. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/descriptron_credentials.py +0 -0
  121. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/descriptron_joints.py +0 -0
  122. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/descriptron_labelled_plates_v1.py +0 -0
  123. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/descriptron_metadata.py +0 -0
  124. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/descriptron_phylo.py +0 -0
  125. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/descriptron_phylo_figure_v1.py +0 -0
  126. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/descriptron_reexamine_v1.py +0 -0
  127. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/descriptron_reexamine_v2.py +0 -0
  128. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/descriptron_rosetta.py +0 -0
  129. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/descriptron_shape_stats.py +0 -0
  130. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/descriptron_trait_stats.py +0 -0
  131. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/dna_vs_morphology_v1.py +0 -0
  132. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/generate_species_plates.py +0 -0
  133. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/landmark_gpa_V1.py +0 -0
  134. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/landmark_gpa_V2.py +0 -0
  135. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/make_reproducibility_bundle.py +0 -0
  136. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/semi_landmark_and_kpts_procrustesV34_GPA.py +0 -0
  137. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/semi_landmark_and_kpts_procrustesV42_GPA.py +0 -0
  138. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/texture_phenomics_from_coco.py +0 -0
  139. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/texture_phenomics_homology.py +0 -0
  140. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/validate_centerline_lengths.py +0 -0
  141. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/validate_phylo_published_waldron2025.py +0 -0
  142. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/validate_phylo_real_tree.py +0 -0
  143. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/validate_phylo_traits.py +0 -0
  144. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/validate_pipeline_gm_vs_geomorph.py +0 -0
  145. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/validate_semilandmarks_vs_geomorph.py +0 -0
  146. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/validate_shape_stats_vs_geomorph.py +0 -0
  147. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/validate_shape_stats_vs_rrpp.py +0 -0
  148. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/src/descriptron_core/tools/validate_texture_glcm.py +0 -0
  149. {descriptron_core-2.7.0 → descriptron_core-2.7.6}/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.0
3
+ Version: 2.7.6
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.0"
7
+ version = "2.7.6"
8
8
  description = "Evidence-tiered species descriptions, keys and audits from phenomic matrices"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ biorag_coded_states_from_coco_v1.py - descriptive characters recorded in the GUI -> the character matrix
4
+ ========================================================================================================
5
+
6
+ The Descriptron GUI (v82+) and the Descriptron-GBIF Annotator store a structure's descriptive characters in
7
+ COCO, as annotations[].attributes = {"texture": "punctate", "setae": "dense", "setae_count": "12"}. This
8
+ script turns them into what the pipeline reads:
9
+
10
+ 1. coded_states_by_specimen.tsv - one row per specimen x structure x character, in the format of
11
+ biorag_descriptive_scoring_v1 (specimen_id, species, category, structure, character, type, state, image)
12
+ plus source=human, so it can be given wherever model-scored states are accepted (--descriptive_matrix);
13
+ 2. a COPY of a Tier-1 matrix folder with the characters appended as ordinary features, so the key builder,
14
+ calibration, novelty and the exports see them:
15
+ categorical one 0/1 column per state ("texture = punctate"), family "coded_state"
16
+ count the number, family "meristic", unit "count"
17
+ A specimen photographed more than once gets the state seen most often (a tie is left out) and the median
18
+ count. A character scored in fewer than --min_species species, or with one state everywhere, is dropped
19
+ and reported, as in biorag_add_discrete_characters_v1.
20
+
21
+ States a taxonomist records are bench observations, so they enter at --tier key by default (model-scored
22
+ states stay at the tier their reliability check allows). The source matrix folder is never written to.
23
+
24
+ python biorag_coded_states_from_coco_v1.py --coco annotations.json [more.json ...] \\
25
+ --group_labels group_labels.csv [--taxon_profile profile.yaml] \\
26
+ --matrix_dir compiled_key_tier --out_dir compiled_key_tier_coded
27
+ (without --matrix_dir only the TSV is written, to --out_dir)
28
+ """
29
+ import argparse
30
+ import json
31
+ import os
32
+ import re
33
+ import shutil
34
+ import sys
35
+ from collections import Counter
36
+ from pathlib import Path
37
+
38
+ import pandas as pd
39
+
40
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
41
+ VERSION = "1.0"
42
+
43
+
44
+ def slug(s):
45
+ return re.sub(r"[^\w,\-]+", "_", str(s).strip()).strip("_")
46
+
47
+
48
+ def _stem(name):
49
+ b = os.path.basename(str(name))
50
+ return os.path.splitext(b)[0] if os.path.splitext(b)[1].lower() in (".tif", ".tiff", ".jpg", ".jpeg", ".png", ".bmp", ".webp") else b
51
+
52
+
53
+ def load_vocabulary():
54
+ """character types (count vs categorical) from the GUI vocabulary, if it can be found"""
55
+ for c in (Path(__file__).resolve().parent / "descriptron_descriptive_characters.json", # pip: beside it
56
+ Path(__file__).resolve().parent.parent / "descriptron_descriptive_characters.json",
57
+ Path(os.environ.get("DESCRIPTRON_GUI_DATA", "/nonexistent")) / "descriptron_descriptive_characters.json"):
58
+ if c.exists():
59
+ return json.load(open(c))
60
+ return {"characters": {}}
61
+
62
+
63
+ def read_states(coco_paths, group_labels, profile, vocab):
64
+ import biorag_feature_policy as pol
65
+ gl = pd.read_csv(group_labels)
66
+ fn_col = "filename" if "filename" in gl.columns else gl.columns[0]
67
+ species_of = {}
68
+ for fn, sp in zip(gl[fn_col], gl["group_label"]):
69
+ species_of[_stem(fn)] = sp; species_of[_stem(fn).replace(" ", "_")] = sp
70
+ aliases = dict(profile.get("category_aliases") or {})
71
+ chars = vocab.get("characters", {})
72
+ rows, unmatched = [], Counter()
73
+ for path in coco_paths:
74
+ d = json.load(open(path))
75
+ cats = {c["id"]: c["name"] for c in d.get("categories", [])}
76
+ imgs = {im["id"]: im for im in d.get("images", [])}
77
+ for a in d.get("annotations", []):
78
+ attrs = a.get("attributes") or {}
79
+ if not attrs:
80
+ continue
81
+ im = imgs.get(a.get("image_id"), {})
82
+ stem = _stem(im.get("file_name") or a.get("image_id"))
83
+ sp = species_of.get(stem) or species_of.get(stem.replace(" ", "_"))
84
+ if sp is None:
85
+ unmatched[stem] += 1
86
+ continue
87
+ cat = cats.get(a.get("category_id"), str(a.get("category_id")))
88
+ cat = aliases.get(cat, cat)
89
+ for ch, st in attrs.items():
90
+ if str(ch).startswith("_") or st in (None, ""):
91
+ continue
92
+ typ = chars.get(ch, {}).get("type", "categorical")
93
+ rows.append({"specimen_id": pol.specimen_id(stem, sp, profile), "species": sp, "category": cat,
94
+ "structure": cat, "character": ch, "type": "count" if typ == "count" else "nominal",
95
+ "state": str(st).strip(), "image": stem, "source": "human"})
96
+ df = pd.DataFrame(rows, columns=["specimen_id", "species", "category", "structure", "character", "type",
97
+ "state", "image", "source"])
98
+ # a character the vocabulary does not know (added by the user, or no vocabulary found) is a count when every
99
+ # value recorded for it is a whole number
100
+ for ch, g in df.groupby("character"):
101
+ if ch not in chars and g["state"].str.fullmatch(r"\d+").all():
102
+ df.loc[g.index, "type"] = "count"
103
+ return df, unmatched
104
+
105
+
106
+ def one_per_specimen(df):
107
+ """modal state (ties left out) / median count per specimen x structure x character"""
108
+ out = []
109
+ for (sid, sp, cat, ch, typ), g in df.groupby(["specimen_id", "species", "category", "character", "type"]):
110
+ if typ == "count":
111
+ v = pd.to_numeric(g["state"], errors="coerce").dropna()
112
+ if len(v):
113
+ out.append((sid, sp, cat, ch, typ, float(v.median())))
114
+ else:
115
+ c = Counter(g["state"].str.lower()).most_common()
116
+ if len(c) == 1 or c[0][1] > c[1][1]:
117
+ out.append((sid, sp, cat, ch, typ, c[0][0]))
118
+ return pd.DataFrame(out, columns=["specimen_id", "species", "category", "character", "type", "state"])
119
+
120
+
121
+ def recorded_states(fd: pd.DataFrame, long: pd.DataFrame) -> dict:
122
+ """the recorded states back from a matrix: {species: {(category, character): {"label", "states": {state: n},
123
+ "n": specimens scored}}}. Shared by the treatment writer and the audit, so both read the same thing."""
124
+ fd = fd.reset_index() if "feature_id" not in fd.columns else fd
125
+ cs = fd[fd["family"] == "coded_state"] if "family" in fd.columns else fd.iloc[0:0]
126
+ if cs.empty:
127
+ return {}
128
+ meta = {}
129
+ for _, r in cs.iterrows():
130
+ cat = r["category"]
131
+ if "character" in cs.columns and isinstance(r.get("character"), str):
132
+ ch, st = r["character"], r["state"]
133
+ lab = r.get("character_label") if isinstance(r.get("character_label"), str) else ch.replace("_", " ")
134
+ else: # older files: "category: character = state"
135
+ left, _, st = str(r["label"]).rpartition(" = ")
136
+ ch = left.split(": ", 1)[-1]; lab = ch.replace("_", " ")
137
+ meta[r["feature_id"]] = (cat, ch, str(st), lab)
138
+ sub = long[long["feature_id"].isin(meta)]
139
+ out = {}
140
+ for (sp, fid), g in sub.groupby(["species", "feature_id"]):
141
+ cat, ch, st, lab = meta[fid]
142
+ rec = out.setdefault(sp, {}).setdefault((cat, ch), {"label": lab, "states": {}, "specimens": set()})
143
+ rec["specimens"] |= set(g["specimen_id"])
144
+ k = int((g["value"] >= 0.5).sum())
145
+ if k:
146
+ rec["states"][st] = rec["states"].get(st, 0) + k
147
+ for sp in out:
148
+ for rec in out[sp].values():
149
+ rec["n"] = len(rec.pop("specimens"))
150
+ return out
151
+
152
+
153
+ def to_features(per, tier, min_species, known, labels=None):
154
+ """feature-dictionary rows and long-matrix rows; returns (fd, long, dropped)"""
155
+ labels = labels or {}
156
+ rows_fd, rows_long, dropped = [], [], {"scored in too few species": [], "same state in every specimen": []}
157
+ per = per[per["specimen_id"].isin(known)] if known is not None else per
158
+ for (cat, ch, typ), g in per.groupby(["category", "character", "type"]):
159
+ name = f"{cat}:{ch}"
160
+ if g["species"].nunique() < min_species:
161
+ dropped["scored in too few species"].append(name); continue
162
+ if g["state"].nunique() < 2:
163
+ dropped["same state in every specimen"].append(name); continue
164
+ lab = labels.get(ch, ch.replace("_", " "))
165
+ if typ == "count":
166
+ feats = [(f"{cat}.coded_{slug(ch)}", f"{cat}: {lab}", "meristic", "count", "",
167
+ g.set_index("specimen_id")["state"].astype(float))]
168
+ else:
169
+ feats = [(f"{cat}.coded_{slug(ch)}__{slug(s)}", f"{cat}: {lab} = {s}", "coded_state", "", s,
170
+ (g.set_index("specimen_id")["state"] == s).astype(float)) for s in sorted(g["state"].unique())]
171
+ meta = g.drop_duplicates("specimen_id").set_index("specimen_id")
172
+ for fid, label, fam, unit, state, vals in feats:
173
+ rows_fd.append({"feature_id": fid, "category": cat, "base_category": cat, "column": "coded_state",
174
+ "tier": tier, "family": fam, "label": label, "character": ch, "state": state,
175
+ "character_label": lab,
176
+ "definition": f"{ch} (recorded by the taxonomist)", "unit": unit, "structure_sex": "both",
177
+ "section": cat, "key_priority": 1, "n_species": int(g["species"].nunique()),
178
+ "n_specimens": int(len(vals)), "conversion": ""})
179
+ for sid, v in vals.items():
180
+ rows_long.append({"species": meta.loc[sid, "species"], "specimen_id": sid, "category": cat,
181
+ "base_category": cat, "column": "coded_state", "feature_id": fid, "tier": tier,
182
+ "family": fam, "value": float(v), "n_images": 1})
183
+ return pd.DataFrame(rows_fd), pd.DataFrame(rows_long), dropped
184
+
185
+
186
+ def main():
187
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
188
+ ap.add_argument("--coco", nargs="+", required=True)
189
+ ap.add_argument("--group_labels", required=True)
190
+ ap.add_argument("--taxon_profile", default=None)
191
+ ap.add_argument("--matrix_dir", default=None, help="Tier-1 matrix folder to copy and extend (optional)")
192
+ ap.add_argument("--out_dir", required=True)
193
+ ap.add_argument("--tier", choices=["key", "description"], default="key")
194
+ ap.add_argument("--min_species", type=int, default=2)
195
+ a = ap.parse_args()
196
+ import biorag_feature_policy as pol
197
+ profile = pol.load_taxon_profile(a.taxon_profile) if a.taxon_profile else {}
198
+ out = Path(a.out_dir)
199
+ if a.matrix_dir and out.resolve() == Path(a.matrix_dir).resolve():
200
+ sys.exit("refusing to write over the source matrix; give a different --out_dir")
201
+ out.mkdir(parents=True, exist_ok=True)
202
+ df, unmatched = read_states(a.coco, a.group_labels, profile, load_vocabulary())
203
+ df.to_csv(out / "coded_states_by_specimen.tsv", sep="\t", index=False)
204
+ print(f"{len(df)} coded states from {df['specimen_id'].nunique() if len(df) else 0} specimens, "
205
+ f"{df['character'].nunique() if len(df) else 0} characters -> {out / 'coded_states_by_specimen.tsv'}")
206
+ if unmatched:
207
+ print(f" {sum(unmatched.values())} annotation(s) on {len(unmatched)} image(s) not in --group_labels, skipped: "
208
+ f"{list(unmatched)[:4]}")
209
+ report = {"version": VERSION, "coco": [str(p) for p in a.coco], "n_states": int(len(df)),
210
+ "unmatched_images": dict(unmatched)}
211
+ if a.matrix_dir:
212
+ src = Path(a.matrix_dir)
213
+ fd = pd.read_csv(src / "feature_dictionary.tsv", sep="\t")
214
+ long = pd.read_csv(src / "specimen_matrix_long.csv")
215
+ known = set(long["specimen_id"])
216
+ per = one_per_specimen(df) if len(df) else df
217
+ stray = sorted(set(per["specimen_id"]) - known) if len(per) else []
218
+ if stray:
219
+ print(f" {len(stray)} specimen(s) with coded states are not in the matrix, skipped: {stray[:4]}")
220
+ labels = {k: v.get("label", k) for k, v in load_vocabulary().get("characters", {}).items()}
221
+ add_fd, add_long, dropped = (to_features(per, a.tier, a.min_species, known, labels) if len(per)
222
+ else (pd.DataFrame(), pd.DataFrame(), {}))
223
+ if len(add_fd):
224
+ sex = dict(zip(long["specimen_id"], long["sex"])) if "sex" in long else {}
225
+ add_long["sex"] = add_long["specimen_id"].map(sex)
226
+ clash = set(add_fd["feature_id"]) & set(fd["feature_id"])
227
+ if clash:
228
+ 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)
234
+ n_key = lambda f: int((f["tier"] == "key").sum()) if len(f) else 0 # noqa: E731
235
+ print(f" matrix: {len(add_fd)} features added ({n_key(add_fd)} at tier key); key features "
236
+ f"{n_key(fd)} -> {n_key(fd) + n_key(add_fd)} -> {out}")
237
+ for why, names in dropped.items():
238
+ if names:
239
+ print(f" dropped ({why}): {names[:6]}{' ...' if len(names) > 6 else ''}")
240
+ report.update(features_added=int(len(add_fd)), dropped=dropped, source_matrix=str(src))
241
+ json.dump(report, open(out / "coded_states_report.json", "w"), indent=1)
242
+
243
+
244
+ if __name__ == "__main__":
245
+ main()
@@ -1251,6 +1251,59 @@ def absence_claims(treat, code, ev: Evidence, issues) -> List[Dict]:
1251
1251
  # Driver
1252
1252
  # ─────────────────────────────────────────────────────────────────────────────
1253
1253
 
1254
+ _NEG = re.compile(r"\b(?:not|never|without|non|no)\W+(?:\w+\W+){0,1}$", re.I)
1255
+
1256
+
1257
+ def verify_coded_states(field: str, text: str, code: str, ev: Evidence, issues: List[Dict]) -> List[Dict]:
1258
+ """descriptive states recorded by the taxonomist (family coded_state): a state named for a structure must be
1259
+ one recorded for this species; a negated state must not be one recorded for it"""
1260
+ try:
1261
+ from biorag_coded_states_from_coco_v1 import recorded_states
1262
+ except Exception:
1263
+ return []
1264
+ if not hasattr(ev, "_coded"):
1265
+ ev._coded = recorded_states(ev.fdict, ev.long)
1266
+ mine = ev._coded.get(code) or {}
1267
+ recs = []
1268
+ if not mine or not text:
1269
+ return recs
1270
+ vocab = defaultdict(set) # (category, character) -> every state recorded in the genus
1271
+ for d in ev._coded.values():
1272
+ for key, r in d.items():
1273
+ vocab[key] |= set(r["states"])
1274
+ for a, b in sentence_spans(text):
1275
+ sent = text[a:b]
1276
+ low = sent.lower()
1277
+ for (cat, ch), r in mine.items():
1278
+ info = pol.structure_info(cat, ev.profile)
1279
+ terms = {info.get("term", ""), info.get("short", ""), cat.replace("_", " ")}
1280
+ if not any(t and re.search(rf"\b{re.escape(t.lower())}\b", low) for t in terms):
1281
+ continue
1282
+ names_char = bool(re.search(rf"\b{re.escape(r['label'].lower())}\b", low)) or \
1283
+ bool(re.search(rf"\b{re.escape(ch.replace('_', ' ').lower())}\b", low))
1284
+ for st in sorted(vocab[(cat, ch)], key=len, reverse=True):
1285
+ for m in re.finditer(rf"\b{re.escape(st.lower())}\b", low):
1286
+ negated = bool(_NEG.search(low[:m.start()]))
1287
+ have = st in r["states"]
1288
+ if negated and have:
1289
+ status, etype = "error", "coded_state_contradicted"
1290
+ note = f"{info['term']} {r['label']}: '{st}' is recorded for this species, the text negates it"
1291
+ elif not negated and not have:
1292
+ status = "error" if names_char else "review"
1293
+ etype = "coded_state_mismatch"
1294
+ note = (f"{info['term']} {r['label']}: '{st}' is not recorded for this species "
1295
+ f"(recorded: {', '.join(r['states'])})")
1296
+ else:
1297
+ status, etype, note = "ok", "", ""
1298
+ rec = dict(kind="coded_state", section=field, status=status, type=etype,
1299
+ feature=f"{cat}:{ch}", note=note, context=sent[:200])
1300
+ recs.append(rec)
1301
+ if status != "ok":
1302
+ issues.append({k: rec[k] for k in ("kind", "type", "feature", "note", "context")} |
1303
+ {"severity": "error" if status == "error" else "warning"})
1304
+ return recs
1305
+
1306
+
1254
1307
  def audit_species(code: str, treat: Dict, sheet: str, ev: Evidence) -> Dict:
1255
1308
  issues: List[Dict] = []
1256
1309
  records: List[Dict] = []
@@ -1273,6 +1326,7 @@ def audit_species(code: str, treat: Dict, sheet: str, ev: Evidence) -> Dict:
1273
1326
  "feature", "status", "type", "note", "context")}))
1274
1327
  records += verify_comparisons(text, comps, claims, code, ev, issues, name, structs, sheet_comps)
1275
1328
  records += verify_wording(name, text, code, ev, issues)
1329
+ records += verify_coded_states(name, text, code, ev, issues)
1276
1330
  records += verify_wording("Remarks", treat.get("remarks", "") or "", code, ev, issues, tier1=False)
1277
1331
  records += verify_remarks(treat.get("remarks", "") or "", code, ev, blocks, issues, sheet_comps)
1278
1332
  records += absence_claims(treat, code, ev, issues)
@@ -159,6 +159,8 @@ def build_sheet(ev: Evidence, code: str) -> Tuple[str, Dict]:
159
159
  meta = fd.loc[fid]
160
160
  if meta["tier"] not in pol.TIER1:
161
161
  continue
162
+ if meta.get("family") == "coded_state": # recorded states: their own section, in words (below)
163
+ continue
162
164
  col = meta["column"]
163
165
  if col.startswith("lmkmm_") or (col.startswith("lmkrel_") and row["n"] < 2):
164
166
  continue # keep sheets readable: landmark ratios with n>=2 only
@@ -219,7 +221,7 @@ def build_sheet(ev: Evidence, code: str) -> Tuple[str, Dict]:
219
221
  comp = []
220
222
  s_all = ev.summary[(ev.summary["n"] >= 2)]
221
223
  for fid, grp in s_all.groupby("feature_id"):
222
- if fid not in fd.index or fd.loc[fid]["tier"] != pol.TIER_KEY:
224
+ if fid not in fd.index or fd.loc[fid]["tier"] != pol.TIER_KEY or fd.loc[fid].get("family") == "coded_state":
223
225
  continue
224
226
  me = grp[grp["species"] == code]
225
227
  if me.empty:
@@ -252,6 +254,13 @@ def build_sheet(ev: Evidence, code: str) -> Tuple[str, Dict]:
252
254
  if not comp:
253
255
  lines.append(" (none — sample sizes too small or ranges overlap)")
254
256
  lines.append("")
257
+ # descriptive characters the taxonomist recorded (GUI / GBIF annotator), in words
258
+ cl, cnums, cphr = coded_lines(ev, code)
259
+ if cl:
260
+ lines.extend(cl)
261
+ allowed_t1 += cnums
262
+ phrases += cphr
263
+ lines.append("")
255
264
  # key characters
256
265
  if ev.key:
257
266
  lines.append("=== CHARACTERS USED FOR THIS SPECIES IN THE IDENTIFICATION KEY (Tier 1) ===")
@@ -297,6 +306,63 @@ def build_sheet(ev: Evidence, code: str) -> Tuple[str, Dict]:
297
306
  {short_name(ev, x) for x in ev.species if x != code}, key=len, reverse=True)}
298
307
 
299
308
 
309
+ def coded_lines(ev: Evidence, code: str):
310
+ """the descriptive states recorded for this species, as words, with how many specimens show each, what the
311
+ other species show, and a pre-worded diagnostic statement where a state is recorded in no other species"""
312
+ try:
313
+ from biorag_coded_states_from_coco_v1 import recorded_states
314
+ except Exception:
315
+ return [], [], []
316
+ if not hasattr(ev, "_coded"):
317
+ ev._coded = recorded_states(ev.fdict, ev.long)
318
+ rec = ev._coded
319
+ mine = rec.get(code) or {}
320
+ if not mine:
321
+ return [], [], []
322
+ fd = ev.fdict
323
+ sec_of = {}
324
+ for fid, m in fd.iterrows():
325
+ if m.get("family") != "coded_state" and m.get("category") not in sec_of:
326
+ sec_of[m.get("category")] = m.get("section")
327
+ secs = ev.profile.get("description_sections") or []
328
+ order = {x: i for i, x in enumerate(secs)}
329
+ lines = ["=== DESCRIPTIVE CHARACTERS RECORDED BY THE TAXONOMIST (Tier 1; state them IN WORDS in the Diagnosis "
330
+ "and the Description, e.g. 'setae dense'; never as numbers or codes) ==="]
331
+ nums, phrases, diag = [], [], []
332
+ by_sec = defaultdict(list)
333
+ for (cat, ch), r in mine.items():
334
+ info = pol.structure_info(cat, ev.profile)
335
+ by_sec[sec_of.get(cat) or info.get("section", "Other structures")].append((cat, ch, r, info))
336
+ for sec in sorted(by_sec, key=lambda x: order.get(x, 99)):
337
+ lines.append(f"[{sec}]")
338
+ for cat, ch, r, info in sorted(by_sec[sec], key=lambda x: (x[0], x[1])):
339
+ states = sorted(r["states"].items(), key=lambda kv: -kv[1])
340
+ if not states:
341
+ continue
342
+ own = ", ".join(f"{st} ({k} of {r['n']} specimens)" for st, k in states)
343
+ nums += [r["n"]] + [k for _, k in states]
344
+ others = defaultdict(list)
345
+ n_other = 0
346
+ for sp, d in rec.items():
347
+ if sp == code or (cat, ch) not in d:
348
+ continue
349
+ n_other += 1
350
+ for st in d[(cat, ch)]["states"]:
351
+ others[st].append(ev.display(sp))
352
+ oth = "; ".join(f"{st} in {join_names(sorted(v))}" for st, v in sorted(others.items()))
353
+ lines.append(f" {info['term']}, {r['label']}: {own}" + (f" | other species: {oth}" if oth else ""))
354
+ for st, _ in states:
355
+ if n_other and st not in others:
356
+ diag.append(f"{info['term']} with {r['label']} {st}: recorded in no other species "
357
+ f"(of {n_other} with this character recorded)")
358
+ nums.append(n_other)
359
+ if diag:
360
+ lines.append(" Diagnostic (states recorded in this species only; usable in the Diagnosis, quote as given):")
361
+ for i, t in enumerate(diag, 1):
362
+ lines.append(f" [D{i}] {t}")
363
+ return lines, [float(x) for x in nums], []
364
+
365
+
300
366
  def short_name(ev: Evidence, code: str) -> str:
301
367
  """'Diaphorina sp. 3' -> 'D. sp. 3' for species lists."""
302
368
  name = ev.display(code)
@@ -380,6 +380,8 @@ def fmt(value: Optional[float], unit: str, ref: Optional[float] = None) -> str:
380
380
  d = decimals_for(unit, ref if ref is not None else value)
381
381
  if unit == "deg":
382
382
  return f"{value:.0f}"
383
+ if unit == "count": # meristic: whole numbers, a half only for a median/threshold
384
+ return f"{value:.0f}" if float(value).is_integer() else f"{value:.1f}"
383
385
  return f"{value:.{d}f}"
384
386
 
385
387
 
@@ -77,9 +77,12 @@ BUILDER_VERSION = "1.0"
77
77
 
78
78
  RESOLUTION = {"mm": 0.005, "mm2": 0.0005, "x": 0.02, "L*": 2.0, "a*": 2.0,
79
79
  "b*": 2.0, "C*": 2.0, "deg": 5.0}
80
+ RESOLUTION["count"] = 1.0 # meristic characters recorded in the GUI (whole numbers)
80
81
  PRIORITY_WEIGHT = {1: 1.0, 2: 0.85, 3: 0.7}
81
82
  DEFAULT_TYPE_WEIGHTS = {"ratio": 1.0, "landmark_ratio": 0.95, "aspect_ratio": 0.9,
82
83
  "length": 0.8, "landmark_mm": 0.75, "colour": 0.6}
84
+ # descriptive states and counts a taxonomist recorded in the GUI (biorag_coded_states_from_coco_v1)
85
+ DEFAULT_TYPE_WEIGHTS.update({"coded_state": 0.9, "meristic": 0.9})
83
86
  SEX_SYMBOL = {"male": "♂", "female": "♀"}
84
87
 
85
88
 
@@ -532,15 +535,21 @@ def template_lead(chars: List[Dict], side: str) -> str:
532
535
  op = OP_TXT[c[f"{side}_operator"]]
533
536
  lo, hi = c[f"{side}_range"]
534
537
  unit = c["unit"]
535
- u = "" if unit == "x" else (" mm" if unit == "mm" else f" {unit}" if unit not in ("L*", "a*", "b*", "C*", "deg") else "")
538
+ u = "" if unit in ("x", "count") else (" mm" if unit == "mm" else f" {unit}" if unit not in ("L*", "a*", "b*", "C*", "deg") else "")
536
539
  pre = (SEX_SYMBOL[c["structure_sex"]] + " ") if c["structure_sex"] in SEX_SYMBOL else ""
537
540
  lab = c["label"]
538
541
  def span(fmt_lo, fmt_hi, suffix=""):
539
542
  return f"{fmt_lo}{suffix}" if fmt_lo == fmt_hi else f"{fmt_lo}–{fmt_hi}{suffix}"
540
543
  if c.get("binary"):
541
544
  # a state, not a measurement: say which, and in how many specimens it was seen
542
- state = "present" if c[f"{side}_operator"] == ">" else "absent"
543
545
  usually = "" if c["perfect_separation"] else "usually "
546
+ if c.get("family") == "coded_state" and " = " in lab:
547
+ # a state the taxonomist recorded ("elytron: setae = dense"): "setae dense" / "setae not dense"
548
+ base, _, st = lab.rpartition(" = ")
549
+ word = st if c[f"{side}_operator"] == ">" else f"not {st}"
550
+ parts.append(f"{pre}{base} {usually}{word} (n = {c[f'{side}_n']})")
551
+ continue
552
+ state = "present" if c[f"{side}_operator"] == ">" else "absent"
544
553
  parts.append(f"{pre}{lab}: {usually}{state} (n = {c[f'{side}_n']})")
545
554
  continue
546
555
  if unit in ("L*", "a*", "b*", "C*"):