descriptron-core 2.7.8__tar.gz → 2.8.0__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 (160) hide show
  1. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/PKG-INFO +1 -1
  2. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/pyproject.toml +1 -1
  3. descriptron_core-2.8.0/src/descriptron_core/tools/annotation_scale_transfer_v1.py +132 -0
  4. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_confabulation_checker_v2.py +11 -1
  5. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_description_refiner_v1.py +20 -0
  6. descriptron_core-2.8.0/src/descriptron_core/tools/biorag_diagnosis_builder_v1.py +243 -0
  7. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/color_phenomics_homology_v2_1.py +52 -7
  8. descriptron_core-2.8.0/src/descriptron_core/tools/estimate_hidden_semilandmarks_v1.py +153 -0
  9. descriptron_core-2.8.0/src/descriptron_core/tools/homology_batch_meanshift_v1.py +101 -0
  10. descriptron_core-2.8.0/src/descriptron_core/tools/homology_grid_ext.py +220 -0
  11. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/run_full_pipeline_v2.py +137 -9
  12. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/semi_landmark_and_kpts_procrustesV34_GPA.py +13 -0
  13. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/semi_landmark_and_kpts_procrustesV42_GPA.py +13 -0
  14. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/texture_phenomics_homology.py +58 -10
  15. descriptron_core-2.8.0/src/descriptron_core/tools/validate_hidden_semilandmarks_v1.py +68 -0
  16. descriptron_core-2.8.0/src/descriptron_core/tools/wing_fringe_character_v1.py +82 -0
  17. descriptron_core-2.8.0/src/descriptron_core/tools/wing_spot_pattern_v1.py +93 -0
  18. descriptron_core-2.8.0/src/descriptron_core/tools/wing_vein_naming_rule_v1.py +174 -0
  19. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/.gitignore +0 -0
  20. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/LICENSE +0 -0
  21. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/NOTICE +0 -0
  22. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/README.md +0 -0
  23. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/__init__.py +0 -0
  24. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/cli.py +0 -0
  25. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/data/biorag_prompts/biorag_system_prompts_v1.txt +0 -0
  26. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/data/biorag_prompts/biorag_system_prompts_v2.txt +0 -0
  27. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/data/biorag_prompts/diaphorina_user_prompts.txt +0 -0
  28. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/data/biorag_prompts/ontologies/aism.obo +0 -0
  29. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/data/biorag_prompts/ontologies/pato.obo +0 -0
  30. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/SDD.xsd +0 -0
  31. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/SDD_EnumLib.xsd +0 -0
  32. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/SDD_TypeLib.xsd +0 -0
  33. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_BasePattern.xsd +0 -0
  34. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_CoreExtensions.xsd +0 -0
  35. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_CoreOntology.xsd +0 -0
  36. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_EnumLib.xsd +0 -0
  37. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_LinneanCore.xsd +0 -0
  38. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_TypeLib.xsd +0 -0
  39. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/w3c-schemata/xml.xsd +0 -0
  40. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/data/biorag_prompts/schemas/taxpub/sample_treatment_deposit_example.xml +0 -0
  41. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/data/biorag_prompts/schemas/taxpub/sample_zookeys_26056_tp.xml +0 -0
  42. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/data/biorag_prompts/schemas/taxpub/tax-treatment-NS0-v1_flat.dtd +0 -0
  43. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/data/biorag_prompts/taxon_profiles/TEMPLATE_taxon_profile.yaml +0 -0
  44. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/data/biorag_prompts/taxon_profiles/diaphorina_taxon_profile.yaml +0 -0
  45. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/FHS_and_CLAHE_V21.py +0 -0
  46. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_add_discrete_characters_v1.py +0 -0
  47. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_annotation_reorient_v1.py +0 -0
  48. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_annotation_screen_v1.py +0 -0
  49. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_autapomorphy_v1.py +0 -0
  50. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_calibrate_v1.py +0 -0
  51. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_calibration_figures_v1.py +0 -0
  52. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_character_gate_v1.py +0 -0
  53. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_character_reliability_v1.py +0 -0
  54. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_character_robustness_v1.py +0 -0
  55. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_coded_states_from_coco_v1.py +0 -0
  56. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_collaborator_forms_v1.py +0 -0
  57. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_computed_sets_v1.py +0 -0
  58. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_congruence_compare_v1.py +0 -0
  59. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_descriptive_scoring_v1.py +0 -0
  60. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_feature_policy.py +0 -0
  61. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_frame_orientation_calibrate_v1.py +0 -0
  62. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_frame_orientation_v1.py +0 -0
  63. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_gate_compare_v1.py +0 -0
  64. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_graph_identify_v1.py +0 -0
  65. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_graph_support_figure_v1.py +0 -0
  66. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_homology_frame_v1.py +0 -0
  67. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_instrument_compare_v3.py +0 -0
  68. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_key_builder_v1.py +0 -0
  69. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_key_feature_filter_v2.py +0 -0
  70. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_key_fuzzy_figures_v1.py +0 -0
  71. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_key_fuzzy_v1.py +0 -0
  72. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_key_holdout_v1.py +0 -0
  73. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_key_jackknife_support_v1.py +0 -0
  74. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_label_conflict_check_v1.py +0 -0
  75. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_llm_backend.py +0 -0
  76. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_make_taxon_profile_v1.py +0 -0
  77. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_methods_summary_figure_v1.py +0 -0
  78. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_novelty_score_v1.py +0 -0
  79. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_ontology_annotator_v2.py +0 -0
  80. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_outgroup_batch_check_v1.py +0 -0
  81. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_outline_shape_v1.py +0 -0
  82. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_prompts/biorag_system_prompts_v1.txt +0 -0
  83. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_prompts/biorag_system_prompts_v2.txt +0 -0
  84. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_prompts/diaphorina_user_prompts.txt +0 -0
  85. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_prompts/ontologies/aism.obo +0 -0
  86. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_prompts/ontologies/pato.obo +0 -0
  87. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/SDD.xsd +0 -0
  88. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/SDD_EnumLib.xsd +0 -0
  89. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/SDD_TypeLib.xsd +0 -0
  90. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_BasePattern.xsd +0 -0
  91. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_CoreExtensions.xsd +0 -0
  92. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_CoreOntology.xsd +0 -0
  93. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_EnumLib.xsd +0 -0
  94. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_LinneanCore.xsd +0 -0
  95. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_TypeLib.xsd +0 -0
  96. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/w3c-schemata/xml.xsd +0 -0
  97. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_prompts/schemas/taxpub/sample_treatment_deposit_example.xml +0 -0
  98. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_prompts/schemas/taxpub/sample_zookeys_26056_tp.xml +0 -0
  99. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_prompts/schemas/taxpub/tax-treatment-NS0-v1_flat.dtd +0 -0
  100. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_prompts/taxon_profiles/TEMPLATE_taxon_profile.yaml +0 -0
  101. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_prompts/taxon_profiles/diaphorina_taxon_profile.yaml +0 -0
  102. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_provenance_v1.py +0 -0
  103. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_rebuild_downstream_v1.sh +0 -0
  104. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_reliability_figures_v1.py +0 -0
  105. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_rename_species_v1.py +0 -0
  106. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_rerun_descriptive_tables_v1.sh +0 -0
  107. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_retest_compare_v1.py +0 -0
  108. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_specimen_id.py +0 -0
  109. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_subjective_checks_v1.py +0 -0
  110. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_type_material_v1.py +0 -0
  111. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_vlm_characters_v1.py +0 -0
  112. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_vlm_heatmap_figures_v1.py +0 -0
  113. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_vlm_reliability_figure_v1.py +0 -0
  114. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biorag_vlm_roi_figures_v1.py +0 -0
  115. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biosyslit_rag_retrieval.py +0 -0
  116. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/biosyslit_rag_retrieval_v2.py +0 -0
  117. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/build_monograph_exports_v1.py +0 -0
  118. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/build_species_treatment_docx_v2.py +0 -0
  119. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/coco_combiner_V13.py +0 -0
  120. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/coco_converter_v24.py +0 -0
  121. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/coi_species_tree_v1.py +0 -0
  122. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/color_extraction_with_names-v8.py +0 -0
  123. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/compile_specimen_data.py +0 -0
  124. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/descriptron_centerline.py +0 -0
  125. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/descriptron_character_signal_v1.py +0 -0
  126. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/descriptron_check_completeness_v1.py +0 -0
  127. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/descriptron_check_cross_image_copies_v1.py +0 -0
  128. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/descriptron_coco_tools.py +0 -0
  129. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/descriptron_community_v1.py +0 -0
  130. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/descriptron_convert.py +0 -0
  131. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/descriptron_credentials.py +0 -0
  132. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/descriptron_descriptive_characters.json +0 -0
  133. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/descriptron_joints.py +0 -0
  134. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/descriptron_labelled_plates_v1.py +0 -0
  135. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/descriptron_metadata.py +0 -0
  136. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/descriptron_phylo.py +0 -0
  137. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/descriptron_phylo_figure_v1.py +0 -0
  138. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/descriptron_reexamine_v1.py +0 -0
  139. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/descriptron_reexamine_v2.py +0 -0
  140. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/descriptron_rosetta.py +0 -0
  141. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/descriptron_shape_stats.py +0 -0
  142. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/descriptron_trait_stats.py +0 -0
  143. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/dna_vs_morphology_v1.py +0 -0
  144. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/generate_species_plates.py +0 -0
  145. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/landmark_gpa_V1.py +0 -0
  146. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/landmark_gpa_V2.py +0 -0
  147. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/make_reproducibility_bundle.py +0 -0
  148. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/texture_phenomics_from_coco.py +0 -0
  149. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/validate_centerline_lengths.py +0 -0
  150. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/validate_community_run_vs_r.py +0 -0
  151. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/validate_community_vs_r.py +0 -0
  152. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/validate_phylo_published_waldron2025.py +0 -0
  153. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/validate_phylo_real_tree.py +0 -0
  154. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/validate_phylo_traits.py +0 -0
  155. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/validate_pipeline_gm_vs_geomorph.py +0 -0
  156. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/validate_semilandmarks_vs_geomorph.py +0 -0
  157. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/validate_shape_stats_vs_geomorph.py +0 -0
  158. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/validate_shape_stats_vs_rrpp.py +0 -0
  159. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/src/descriptron_core/tools/validate_texture_glcm.py +0 -0
  160. {descriptron_core-2.7.8 → descriptron_core-2.8.0}/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.8
3
+ Version: 2.8.0
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.8"
7
+ version = "2.8.0"
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,132 @@
1
+ #!/usr/bin/env python3
2
+ """Annotate huge images on smaller copies, then put the annotations back on the originals (17 Oct 2026; general, any taxon).
3
+
4
+ shrink --images DIR [--list FILE] --out DIR [--max_side 2700 | --scale 0.5] [--quality 95]
5
+ writes a JPG copy of every image (aspect kept) + scale_map.tsv (small file -> original path and both sizes)
6
+ to_small --coco FULL.json --scale_map scale_map.tsv --out SMALL.json
7
+ existing full-resolution annotations (e.g. predictions to correct) -> coordinates of the small copies
8
+ to_full --coco SMALL.json(.rlejson) --scale_map scale_map.tsv --out FULL.json
9
+ annotations made on the small copies -> original image names, sizes and coordinates; writes FULL.json (polygons)
10
+ AND FULL.rlejson (RLE, keeps holes), as the GUI does
11
+
12
+ Masks are scaled as CONTOURS (outer + holes, sub-pixel scaled, refilled at the target size), not by nearest-neighbour upsampling,
13
+ so edges stay smooth. Keypoints, keypoints_clicked, bbox, area, line_points and line_thickness are scaled; every other field is copied.
14
+ A mask made at scale s can only be as precise as 1/s original pixels: at 0.5 that is about 2 px on the original."""
15
+ import argparse, csv, json, os, sys, numpy as np, cv2
16
+ from PIL import Image
17
+ Image.MAX_IMAGE_PIXELS = None
18
+ EXT = ('.tif', '.tiff', '.png', '.jpg', '.jpeg', '.bmp')
19
+
20
+
21
+ def shrink(a):
22
+ os.makedirs(a.out, exist_ok=True)
23
+ files = [l.strip() for l in open(a.list) if l.strip()] if a.list else sorted(f for f in os.listdir(a.images) if f.lower().endswith(EXT) and not f.startswith('._') and '_mask' not in f)
24
+ rows = []
25
+ for f in files:
26
+ p = f if os.path.isabs(f) else os.path.join(a.images, f); im = Image.open(p); W, H = im.size
27
+ s = a.scale if a.scale else min(1.0, a.max_side / max(W, H)); w, h = max(1, round(W * s)), max(1, round(H * s))
28
+ small = os.path.splitext(os.path.basename(p))[0] + '.jpg'
29
+ im.convert('RGB').resize((w, h), Image.LANCZOS).save(os.path.join(a.out, small), quality=a.quality)
30
+ rows.append((small, os.path.abspath(p), W, H, w, h)); print(f'{small}: {W}x{H} -> {w}x{h}', flush=True)
31
+ with open(os.path.join(a.out, 'scale_map.tsv'), 'w') as fh:
32
+ fh.write('small_file\toriginal_path\torig_width\torig_height\tsmall_width\tsmall_height\n')
33
+ for r in rows: fh.write('\t'.join(map(str, r)) + '\n')
34
+ print(len(rows), 'images ->', a.out)
35
+
36
+
37
+ def load_map(p):
38
+ M = {}
39
+ for r in csv.DictReader(open(p), delimiter='\t'):
40
+ d = dict(small=r['small_file'], orig=os.path.basename(r['original_path']), W=int(r['orig_width']), H=int(r['orig_height']), w=int(r['small_width']), h=int(r['small_height']))
41
+ for k in (d['small'], d['orig'], os.path.splitext(d['small'])[0], os.path.splitext(d['orig'])[0]): M[k] = d
42
+ return M
43
+
44
+
45
+ def seg_to_mask(seg, h, w):
46
+ from pycocotools import mask as mu
47
+ if isinstance(seg, dict):
48
+ r = dict(seg)
49
+ if isinstance(r['counts'], list): r = mu.frPyObjects(r, h, w)
50
+ elif isinstance(r['counts'], str): r['counts'] = r['counts'].encode('ascii')
51
+ return mu.decode(r).astype(np.uint8)
52
+ m = np.zeros((h, w), np.uint8)
53
+ polys = [np.asarray(p, np.float64).reshape(-1, 2) for p in seg if len(p) >= 6]
54
+ if polys: cv2.fillPoly(m, [np.round(p).astype(np.int32) for p in polys], 1)
55
+ return m
56
+
57
+
58
+ def scale_mask(m, sx, sy, W, H):
59
+ """contours (with holes) scaled about pixel centres, refilled at W x H with 8x sub-pixel precision"""
60
+ cs, hier = cv2.findContours(m.astype(np.uint8), cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE); out = np.zeros((H, W), np.uint8)
61
+ if not cs: return out
62
+ SH = 3; f = 1 << SH
63
+ def sc(c): q = c.reshape(-1, 2).astype(np.float64); q[:, 0] = (q[:, 0] + 0.5) * sx - 0.5; q[:, 1] = (q[:, 1] + 0.5) * sy - 0.5; return np.round(q * f).astype(np.int32)
64
+ for i, c in enumerate(cs):
65
+ if hier[0][i][3] < 0: cv2.fillPoly(out, [sc(c)], 1, lineType=cv2.LINE_8, shift=SH)
66
+ for i, c in enumerate(cs):
67
+ if hier[0][i][3] >= 0: cv2.fillPoly(out, [sc(c)], 0, lineType=cv2.LINE_8, shift=SH)
68
+ return out
69
+
70
+
71
+ def rle(m):
72
+ from pycocotools import mask as mu
73
+ r = mu.encode(np.asfortranarray(m.astype(np.uint8))); r['counts'] = r['counts'].decode('ascii'); return r
74
+
75
+
76
+ def polys(m):
77
+ cs, _ = cv2.findContours(m.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
78
+ return [c.reshape(-1).astype(float).tolist() for c in cs if len(c) >= 3]
79
+
80
+
81
+ def scale_kp(v, sx, sy):
82
+ if isinstance(v, str):
83
+ try: v = json.loads(v)
84
+ except Exception: return v
85
+ if not isinstance(v, list): return v
86
+ if v and isinstance(v[0], (list, tuple)): return [[p[0] * sx, p[1] * sy] + list(p[2:]) for p in v]
87
+ out = list(v)
88
+ for i in range(0, len(out) - 2, 3): out[i] = out[i] * sx; out[i + 1] = out[i + 1] * sy
89
+ return out
90
+
91
+
92
+ def convert(a, direction):
93
+ M = load_map(a.scale_map); d = json.load(open(a.coco)); img_new = {}; key = {}
94
+ for im in d['images']:
95
+ m = M.get(os.path.basename(im['file_name'])) or M.get(os.path.splitext(os.path.basename(im['file_name']))[0])
96
+ if m is None: sys.exit(f"image {im['file_name']} not in {a.scale_map}")
97
+ new = dict(im)
98
+ if direction == 'to_full': new.update(file_name=m['orig'], width=m['W'], height=m['H']); sx, sy, tw, th = m['W'] / m['w'], m['H'] / m['h'], m['W'], m['H']; sh, sw = im.get('height', m['h']), im.get('width', m['w'])
99
+ else: new.update(file_name=m['small'], width=m['w'], height=m['h']); sx, sy, tw, th = m['w'] / m['W'], m['h'] / m['H'], m['w'], m['h']; sh, sw = im.get('height', m['H']), im.get('width', m['W'])
100
+ if isinstance(im['id'], str) and os.path.splitext(os.path.basename(im['file_name']))[0] in im['id']:
101
+ new['id'] = im['id'].replace(os.path.splitext(os.path.basename(im['file_name']))[0], os.path.splitext(new['file_name'])[0])
102
+ key[im['id']] = (new['id'], sx, sy, tw, th, sh, sw); img_new[im['id']] = new
103
+ P = {k: v for k, v in d.items() if k not in ('images', 'annotations')}; R = dict(P)
104
+ P['images'] = R['images'] = list(img_new.values()); P['annotations'] = []; R['annotations'] = []
105
+ for an in d['annotations']:
106
+ iid, sx, sy, tw, th, sh, sw = key[an['image_id']]; b = dict(an, image_id=iid)
107
+ for k in ('keypoints', 'keypoints_clicked', 'line_points'):
108
+ if k in b: b[k] = scale_kp(b[k], sx, sy)
109
+ if 'line_thickness' in b and isinstance(b['line_thickness'], (int, float)): b['line_thickness'] = max(1, round(b['line_thickness'] * (sx + sy) / 2))
110
+ if isinstance(b.get('bbox'), list) and len(b['bbox']) == 4: x, y, w, h = b['bbox']; b['bbox'] = [x * sx, y * sy, w * sx, h * sy]
111
+ seg = an.get('segmentation')
112
+ if seg:
113
+ m = scale_mask(seg_to_mask(seg, sh, sw), sx, sy, tw, th)
114
+ b['area'] = float(m.sum());
115
+ if m.any(): b['bbox'] = [float(v) for v in cv2.boundingRect(m)]
116
+ P['annotations'].append(dict(b, segmentation=polys(m))); R['annotations'].append(dict(b, segmentation=rle(m)))
117
+ else:
118
+ if isinstance(b.get('area'), (int, float)) and b['area'] > 1: b['area'] = b['area'] * sx * sy
119
+ P['annotations'].append(b); R['annotations'].append(b)
120
+ base = os.path.splitext(a.out)[0]
121
+ json.dump(P, open(base + '.json', 'w')); json.dump(R, open(base + '.rlejson', 'w'))
122
+ print(f"{len(d['images'])} images, {len(d['annotations'])} annotations -> {base}.json (polygons) + {base}.rlejson (RLE)")
123
+
124
+
125
+ if __name__ == '__main__':
126
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter); sp = ap.add_subparsers(dest='cmd', required=True)
127
+ s = sp.add_parser('shrink'); s.add_argument('--images', required=True); s.add_argument('--list', default=''); s.add_argument('--out', required=True)
128
+ s.add_argument('--max_side', type=int, default=2700); s.add_argument('--scale', type=float, default=0); s.add_argument('--quality', type=int, default=95)
129
+ for c in ('to_small', 'to_full'):
130
+ s = sp.add_parser(c); s.add_argument('--coco', required=True); s.add_argument('--scale_map', required=True); s.add_argument('--out', required=True)
131
+ a = ap.parse_args()
132
+ shrink(a) if a.cmd == 'shrink' else convert(a, a.cmd)
@@ -769,7 +769,17 @@ def verify_measurement(c: Claim, code: str, ev: Evidence, cits, issues: List[Dic
769
769
  elif matching and not ctx_fid:
770
770
  cand = [f for f in matching if f.split(".", 1)[0] in mentioned] or matching
771
771
  target = cand[0]
772
- if len(cand) > 1 and len({f.split('.', 1)[1] for f in cand}) > 1:
772
+ # 2.7.9: a single value in a key-couplet context that is a key threshold is that threshold, even when
773
+ # it happens to equal some other measurement of the species (e.g. 'L* > 58 ... key couplet 20b')
774
+ key_thr = None
775
+ if hi is None and c.get("key_context") and ev.key:
776
+ for r_ in ev.key["couplets"]:
777
+ for ch_ in r_["characters"]:
778
+ if matches(lo, ch_.get("threshold")):
779
+ key_thr = (ev.canon_fid(ch_["feature_id"]), r_["number"])
780
+ if key_thr is not None:
781
+ target, note = key_thr[0], f"key couplet {key_thr[1]} threshold"
782
+ elif len(cand) > 1 and len({f.split('.', 1)[1] for f in cand}) > 1:
773
783
  status, etype, note = "review", "unresolved_attribution", f"value fits {', '.join(cand[:3])}"
774
784
  else:
775
785
  note = "uncited; matched by value"
@@ -689,6 +689,16 @@ def codes_to_check(profile: Dict) -> List[str]:
689
689
  return out
690
690
 
691
691
 
692
+ def deterministic_diagnosis(args, code):
693
+ """2.7.9: the diagnosis from biorag_diagnosis_builder_v1 (--deterministic_diagnosis diagnoses.json), or None"""
694
+ path = getattr(args, "deterministic_diagnosis", None)
695
+ if not path:
696
+ return None
697
+ if getattr(args, "_det_cache", None) is None:
698
+ args._det_cache = json.loads(Path(path).read_text())
699
+ return args._det_cache.get(code)
700
+
701
+
692
702
  def refine_one(code, ev, client, prompts, args, out_dir, log, cev=None):
693
703
  sp_dir = out_dir / code
694
704
  sp_dir.mkdir(parents=True, exist_ok=True)
@@ -786,6 +796,13 @@ def refine_one(code, ev, client, prompts, args, out_dir, log, cev=None):
786
796
  history.append({"attempt": "merged into the existing treatment", "problems": probs})
787
797
  cleaned = clean_treatment(treat, cev) if treat else {}
788
798
  removed = strip_bad_sentences(treat, allowed, codes_to_check(ev.profile)) if probs and treat else []
799
+ det = deterministic_diagnosis(args, code) if treat else None
800
+ if det: # 2.7.9: the diagnosis written by fixed rules, verbatim
801
+ treat["diagnosis"] = det["text"]
802
+ treat["diagnosis_source"] = "deterministic (biorag_diagnosis_builder_v1): key path + lineage-specific characters"
803
+ treat["citations"] = (treat.get("citations") or []) + \
804
+ [c for c in det.get("citations", []) if c not in (treat.get("citations") or [])]
805
+ allowed["t1"] = list(allowed.get("t1", [])) + [float(x) for x in re.findall(r"\d+(?:\.\d+)?", det["text"])]
789
806
  final = (validate(treat, allowed, codes_to_check(ev.profile), sections) +
790
807
  independent_problems(code, treat, sheet, cev)) if treat else probs
791
808
  treat["_validation"] = {"attempts": history, "final_problems": final, "removed_sentences": removed,
@@ -839,6 +856,9 @@ def main():
839
856
  ap.add_argument("--added_by", default=None,
840
857
  help="Who is adding those sections, e.g. 'Smith, 2031'. Recorded per section so "
841
858
  "a reader can tell the original description from what was added later")
859
+ ap.add_argument("--deterministic_diagnosis", default=None,
860
+ help="diagnoses.json from biorag_diagnosis_builder_v1: its text replaces the model's diagnosis "
861
+ "verbatim (key path + lineage-specific characters, written by fixed rules)")
842
862
  ap.add_argument("--repair_existing", action="store_true",
843
863
  help="Audit existing treatments and send only the flagged problems back to the model "
844
864
  "(the previous JSON is kept as <code>_treatment_before_repair_<date>.json)")
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ biorag_diagnosis_builder_v1.py - a deterministic diagnosis per species, from the key and the matrix (no model)
4
+ ==========================================================================================================
5
+
6
+ The model-written diagnosis (biorag_description_refiner_v1) chooses which of the supplied true statements to lead
7
+ with, so two runs can stress different characters. This program writes the diagnosis itself, by fixed rules, so the
8
+ same matrix and key always give the same text:
9
+
10
+ 1. KEY CHARACTERS - the couplets on the species' own path through the identification key (biorag_key_builder_v1),
11
+ in key order; for each couplet its primary character, worded as the key words it, but with THIS species'
12
+ observed range and n (a key couplet describes a group of species).
13
+ 2. LINEAGE-SPECIFIC CHARACTERS
14
+ - categorical states and presence / absence recorded by the taxonomist (GUI descriptive characters, Record
15
+ absent, Lost keypoints): a state recorded in no other species, or in at most --max_shared others (named);
16
+ - meristic characters (counts): the species' range does not overlap that of at least half of the other species
17
+ scored (n >= 2 in both), with the species named, as in the comparative statements;
18
+ - measurements whose range overlaps no other species scored (n >= 2 in both): unique values.
19
+ Every number is the species' own specimen range from specimen_matrix_long.csv; every species list is written by
20
+ the same function as the comparative statements, so the independent audit (biorag_confabulation_checker_v2) can
21
+ re-derive each claim.
22
+
23
+ python biorag_diagnosis_builder_v1.py --matrix_dir compiled_key_tier --key_tree key_v2/key_tree.json \\
24
+ --taxon_profile profile.yaml --out_dir diagnoses/
25
+
26
+ Writes diagnoses.json ({species: {text, items: [{source, feature_id, couplet, text}], words}}), diagnoses.md and
27
+ diagnoses.tsv. biorag_description_refiner_v1 --deterministic_diagnosis diagnoses.json uses the text verbatim in
28
+ place of the model's diagnosis (run_full_pipeline_v2 --deterministic_diagnosis does both).
29
+ """
30
+ import argparse
31
+ import copy
32
+ import json
33
+ import re
34
+ import sys
35
+ import types
36
+ from pathlib import Path
37
+
38
+ import numpy as np
39
+
40
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
41
+ import biorag_feature_policy as pol # noqa: E402
42
+ import biorag_description_refiner_v1 as rf # noqa: E402
43
+ import biorag_key_builder_v1 as kb # noqa: E402
44
+
45
+ VERSION = "1.0"
46
+
47
+
48
+ class _NS(types.SimpleNamespace):
49
+ def __getattr__(self, k):
50
+ return None
51
+
52
+
53
+ def species_values(ev, code, fid):
54
+ v = ev.long.loc[(ev.long["species"] == code) & (ev.long["feature_id"] == fid), "value"].dropna()
55
+ return v.to_numpy(dtype=float)
56
+
57
+
58
+ def key_items(ev, code, max_group=6):
59
+ """the primary character of each couplet on the species' path whose side holds at most max_group species (the
60
+ couplets that separate it from its nearest species; the same rule as the data sheet), with its own range"""
61
+ items = []
62
+ if not ev.key:
63
+ return items
64
+ max_group = min(max_group, max(1, len(ev.species) // 4)) # scales with the genus: 6 of 29, 1 of 4
65
+ for r in sorted(ev.key["couplets"], key=lambda r: r["number"]):
66
+ for side in ("A", "B"):
67
+ if code not in r[f"{side}_species"] or (r[f"{side}_goto"] != code and
68
+ len(r[f"{side}_species"]) > max_group):
69
+ continue
70
+ chars = sorted(r["characters"], key=lambda c: c.get("role") != "primary")
71
+ for c in chars: # the first character this species was scored for
72
+ vals = species_values(ev, code, c["feature_id"])
73
+ if not len(vals):
74
+ continue
75
+ c2 = copy.deepcopy(c)
76
+ c2[f"{side}_range"] = [float(vals.min()), float(vals.max())]
77
+ c2[f"{side}_n"] = int(len(vals))
78
+ thr = c["threshold"]
79
+ ok = (vals <= thr) if c[f"{side}_operator"] == "<=" else (vals > thr)
80
+ c2["perfect_separation"] = bool(ok.all())
81
+ c2.pop(f"{side}_colour_term", None) # the group's colour word; not this species'
82
+ txt = kb.template_lead([c2], side)
83
+ where = f"key couplet {r['number']}{side.lower()}"
84
+ txt = txt[:-1] + f"; {where})" if txt.endswith(")") else f"{txt} ({where})"
85
+ m = re.search(r"observed ([^;)]+)", txt)
86
+ cit = [{"value": m.group(1).strip(), "id": c["feature_id"]}] if m else []
87
+ items.append({"source": "key", "couplet": f"{r['number']}{side.lower()}",
88
+ "feature_id": c["feature_id"], "text": txt, "citations": cit})
89
+ break
90
+ return items
91
+
92
+
93
+ def categorical_items(ev, code, max_shared):
94
+ from biorag_coded_states_from_coco_v1 import recorded_states
95
+ rec = recorded_states(ev.fdict, ev.long)
96
+ mine = rec.get(code) or {}
97
+ items = []
98
+ for (cat, ch), r in sorted(mine.items()):
99
+ scored = [sp for sp in rec if sp != code and (cat, ch) in rec[sp]]
100
+ if len(scored) < 2:
101
+ continue
102
+ term = pol.structure_info(cat, ev.profile).get("term", cat.replace("_", " "))
103
+ for st, k in sorted(r["states"].items(), key=lambda kv: -kv[1]):
104
+ with_ = sorted([sp for sp in scored if st in rec[sp][(cat, ch)]["states"]], key=rf.natural_key)
105
+ if len(with_) > max_shared or len(with_) >= len(scored) / 2: # specific: few, and under half
106
+ continue
107
+ what = f"{term} {st}" if ch == "presence" else f"{term} {r['label']} {st}"
108
+ count = f"{k} of {r['n']} specimens"
109
+ if not with_:
110
+ tail = f"recorded in no other species, {len(scored)} scored"
111
+ else:
112
+ tail = "otherwise recorded only in " + rf.join_names([rf.short_name(ev, x) for x in with_])
113
+ fid = f"{cat}.coded_{pol_slug(ch)}__{pol_slug(st)}"
114
+ items.append({"source": "categorical", "feature_id": fid, "category": cat, "character": ch,
115
+ "unique": not with_, "text": f"{what} ({count}; {tail})"})
116
+ items.sort(key=lambda x: (not x["unique"], x["text"]))
117
+ return items
118
+
119
+
120
+ def pol_slug(s):
121
+ from biorag_coded_states_from_coco_v1 import slug
122
+ return slug(s)
123
+
124
+
125
+ def range_items(ev, code, family_filter, min_share, label_source):
126
+ """non-overlapping specimen ranges (n >= 2 in both), as in the comparative statements"""
127
+ fd = ev.fdict
128
+ s_all = ev.summary[ev.summary["n"] >= 2]
129
+ out = []
130
+ for fid, grp in s_all.groupby("feature_id"):
131
+ if fid not in fd.index or not family_filter(fd.loc[fid]):
132
+ continue
133
+ me = grp[grp["species"] == code]
134
+ if me.empty:
135
+ continue
136
+ me = me.iloc[0]
137
+ others = [x for x in grp["species"] if x != code]
138
+ if len(others) < 2:
139
+ continue
140
+ lower = sorted(grp[(grp["species"] != code) & (grp["max"] < me["min"])]["species"], key=rf.natural_key)
141
+ higher = sorted(grp[(grp["species"] != code) & (grp["min"] > me["max"])]["species"], key=rf.natural_key)
142
+ sep = len(lower) + len(higher)
143
+ if sep < max(1, int(np.ceil(min_share * len(others)))):
144
+ continue
145
+ meta = fd.loc[fid]
146
+ unit = meta["unit"] if isinstance(meta["unit"], str) else ""
147
+ rng = rf.fmt_range(me, unit)[0].split(" (")[0]
148
+ count = meta.get("family") == "meristic"
149
+ up, down = ("more", "fewer") if count else ("greater", "smaller")
150
+ if meta.get("family") == "colour":
151
+ up, down = "paler (higher L*)", "darker (lower L*)"
152
+ # the data sheet's own wording (species_list_phrase), so the audit re-derives the same statement
153
+ clauses = [f"{word} than in {rf.species_list_phrase(ev, group, others)}"
154
+ for group, word in ((lower, up), (higher, down)) if group]
155
+ term = meta["label"]
156
+ out.append({"source": label_source, "feature_id": fid, "separated": sep, "of": len(others),
157
+ "text": f"{term} {rng} (n = {int(me['n'])}): " + "; ".join(clauses),
158
+ "citations": [{"value": rng, "id": fid}]})
159
+ out.sort(key=lambda x: (-x["separated"] / max(x["of"], 1), x["feature_id"]))
160
+ return out
161
+
162
+
163
+ def build(ev, code, max_shared=2, meristic_share=0.5, max_unique_measurements=3, key_max_group=6):
164
+ keys = key_items(ev, code, key_max_group)
165
+ cats = categorical_items(ev, code, max_shared)
166
+ # a categorical state already used by the key is not repeated: its 'recorded in no other species' note is
167
+ # appended to the key item instead
168
+ key_cc = {}
169
+ for it in keys:
170
+ if it["feature_id"] in ev.fdict.index and ev.fdict.loc[it["feature_id"]].get("family") == "coded_state":
171
+ key_cc[(ev.fdict.loc[it["feature_id"]]["category"], ev.fdict.loc[it["feature_id"]].get("character"))] = it
172
+ rest = []
173
+ for c in cats:
174
+ k = key_cc.get((c["category"], c["character"]))
175
+ if k is not None:
176
+ tail = c["text"][c["text"].rfind("; ") + 2:-1]
177
+ k["text"] = k["text"][:-1] + f"; {tail})"
178
+ else:
179
+ rest.append(c)
180
+ used = {it["feature_id"] for it in keys}
181
+ meris = [m for m in range_items(ev, code, lambda m: m.get("family") == "meristic", meristic_share, "meristic")
182
+ if m["feature_id"] not in used]
183
+ uniq = [m for m in range_items(ev, code, lambda m: m.get("tier") == pol.TIER_KEY and
184
+ m.get("family") not in ("meristic", "coded_state"), 1.0, "unique_measurement")
185
+ if m["feature_id"] not in used][:max_unique_measurements]
186
+ n_other = len(ev.species) - 1
187
+ name = ev.display(code)
188
+ parts = []
189
+ if keys:
190
+ parts.append(f"{name} is separated from its nearest species in the key by: "
191
+ + "; ".join(it["text"] for it in keys) + ".")
192
+ lin = rest + meris + uniq
193
+ if lin: # one sentence per character (lists contain ';')
194
+ parts.append(("Further diagnostic characters: " if keys else
195
+ f"{name} differs from the other {n_other} species examined by the following characters: ")
196
+ + " ".join(f"{it['text'][0].upper()}{it['text'][1:]}." for it in lin))
197
+ text = " ".join(parts) or f"No character separates {name} from the other species in the current matrix."
198
+ cits = [c for it in keys + lin for c in it.get("citations", [])]
199
+ return {"text": text, "items": keys + lin, "citations": cits, "words": len(text.split())}
200
+
201
+
202
+ def main():
203
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
204
+ ap.add_argument("--matrix_dir", required=True)
205
+ ap.add_argument("--key_tree", required=True, help="key_tree.json from biorag_key_builder_v1")
206
+ ap.add_argument("--taxon_profile", required=True)
207
+ ap.add_argument("--species", nargs="*", default=None, help="only these species codes (default: all)")
208
+ ap.add_argument("--max_shared", type=int, default=2,
209
+ help="a categorical state counts as lineage-specific when recorded in at most this many other "
210
+ "species (named); 0 = only states recorded in no other species (default 2)")
211
+ ap.add_argument("--meristic_share", type=float, default=0.5,
212
+ help="a count is reported when its range does not overlap this share of the other species (0.5)")
213
+ ap.add_argument("--max_unique_measurements", type=int, default=3)
214
+ ap.add_argument("--key_max_group", type=int, default=6,
215
+ help="use a key couplet when the species' side holds at most this many species (default 6)")
216
+ ap.add_argument("--out_dir", required=True)
217
+ a = ap.parse_args()
218
+ profile = pol.load_taxon_profile(a.taxon_profile)
219
+ ev = rf.Evidence(_NS(matrix_dir=a.matrix_dir, key_tree=a.key_tree), profile)
220
+ out = Path(a.out_dir); out.mkdir(parents=True, exist_ok=True)
221
+ res = {}
222
+ for code in (a.species or ev.species):
223
+ res[code] = build(ev, code, a.max_shared, a.meristic_share, a.max_unique_measurements, a.key_max_group)
224
+ meta = {"version": VERSION, "matrix_dir": str(a.matrix_dir), "key_tree": str(a.key_tree),
225
+ "max_shared": a.max_shared, "meristic_share": a.meristic_share}
226
+ json.dump({"_meta": meta, **res}, open(out / "diagnoses.json", "w"), indent=1, ensure_ascii=False)
227
+ with open(out / "diagnoses.md", "w") as f:
228
+ f.write("# Diagnoses (deterministic: key path + lineage-specific characters)\n\n")
229
+ for code, r in res.items():
230
+ f.write(f"**{ev.display(code)}.** {r['text']}\n\n")
231
+ with open(out / "diagnoses.tsv", "w") as f:
232
+ f.write("species\tsource\tcouplet\tfeature_id\ttext\n")
233
+ for code, r in res.items():
234
+ for it in r["items"]:
235
+ f.write(f"{code}\t{it['source']}\t{it.get('couplet', '')}\t{it['feature_id']}\t{it['text']}\n")
236
+ n_k = sum(any(i["source"] == "key" for i in r["items"]) for r in res.values())
237
+ n_l = sum(any(i["source"] != "key" for i in r["items"]) for r in res.values())
238
+ print(f"{len(res)} diagnoses: {n_k} with key characters, {n_l} with lineage-specific characters; median "
239
+ f"{int(np.median([r['words'] for r in res.values()]))} words -> {out / 'diagnoses.md'}")
240
+
241
+
242
+ if __name__ == "__main__":
243
+ main()
@@ -607,7 +607,8 @@ def combine_pattern_and_hue(X_pattern, X_hue, hue_weight=1.0):
607
607
  ###############################################################################
608
608
 
609
609
  def process_specimen(fn_gpa, specimen_contour, mean_shape, grid_info,
610
- image_dir, mask_lookup, ctrl_step=5, color_bins=8):
610
+ image_dir, mask_lookup, ctrl_step=5, color_bins=8,
611
+ occluder_lookup=None, coverage_out=None):
611
612
  """Process one specimen: TPS warp grid, extract per-cell color."""
612
613
 
613
614
  # resolve image path
@@ -632,6 +633,16 @@ def process_specimen(fn_gpa, specimen_contour, mean_shape, grid_info,
632
633
  logging.warning(f" No mask for {orig_fn}")
633
634
  return None
634
635
 
636
+ # 2026-10-12 (opt-in): occluders removed -> features from visible pixels only; see homology_grid_ext.py
637
+ _expected = None
638
+ if occluder_lookup is not None:
639
+ from homology_grid_ext import visible_and_expected
640
+ fg_mask, _expected = visible_and_expected(fg_mask, occluder_lookup.get(orig_fn), decode_mask)
641
+ if fg_mask.sum() < 100:
642
+ logging.warning(f" Too little visible foreground for {orig_fn}")
643
+ return None
644
+ _cov = []
645
+
635
646
  fg_bool = fg_mask > 0
636
647
 
637
648
  # specimen-wide L*a*b* means for relative color
@@ -693,8 +704,12 @@ def process_specimen(fn_gpa, specimen_contour, mean_shape, grid_info,
693
704
  for c in range(n_cols):
694
705
  if not cell_inside[r, c]:
695
706
  cell_features.append([np.nan] * n_feats)
707
+ _cov.append(np.nan)
696
708
  continue
697
709
  cm = build_cell_mask(warped_ints, r, c, h, w, fg_mask)
710
+ if _expected is not None:
711
+ _e = build_cell_mask(warped_ints, r, c, h, w, _expected).sum()
712
+ _cov.append(cm.sum() / _e if _e else np.nan)
698
713
  feats = extract_cell_color_features(img_lab, cm,
699
714
  specimen_lab_means,
700
715
  n_color_bins=color_bins)
@@ -704,6 +719,8 @@ def process_specimen(fn_gpa, specimen_contour, mean_shape, grid_info,
704
719
  cell_features.append(feats)
705
720
 
706
721
  fv = np.array(cell_features).flatten()
722
+ if coverage_out is not None and _expected is not None:
723
+ coverage_out[fn_gpa] = np.array(_cov, dtype=float)
707
724
  # v2: 6-tuple now — trailing hue_feats is the specimen-level block.
708
725
  return (fn_gpa, fv, img, fg_mask, warped_ints, hue_feats)
709
726
 
@@ -824,7 +841,18 @@ def parse_args():
824
841
  p.add_argument('--workers', type=int, default=0)
825
842
  p.add_argument('--n_clusters', type=int, default=0,
826
843
  help='Number of clusters (0 = auto via silhouette score)')
827
- p.add_argument('--viz_specimens', type=int, default=5)
844
+ p.add_argument('--viz_specimens', type=int, default=5,
845
+ help='Number of per-specimen grid overlays to save; -1 = every specimen')
846
+ # 2026-10-12 opt-in additions (homology_grid_ext.py); defaults keep the previous behaviour
847
+ p.add_argument('--grid_ext', type=int, default=0,
848
+ help='Extend the grid by N cells on every side and keep cells that overlap the outline '
849
+ '(>= --min_cell_overlap) instead of requiring the cell centre inside. 0 = previous grid')
850
+ p.add_argument('--min_cell_overlap', type=float, default=0.02,
851
+ help='With --grid_ext: minimum fraction of a cell inside the consensus outline (default 0.02)')
852
+ p.add_argument('--occluder_categories', type=str, default='',
853
+ help='Comma-separated COCO categories that hide the structure (e.g. scape,antenna,leg), or "auto" '
854
+ '(categories flagged as occluders in the COCO file, GUI v86). They are removed from the mask '
855
+ '(visible pixels only) and a per-cell visible fraction is written. Nested parts are never used')
828
856
  p.add_argument('--umap_n_neighbors', type=int, default=15)
829
857
  p.add_argument('--color_bins', type=int, default=8,
830
858
  help='Number of bins for within-cell color entropy '
@@ -877,8 +905,19 @@ def main():
877
905
  f"{len(mask_lookup)} masks available")
878
906
 
879
907
  # --- Build grid ---
880
- grid_info = create_grid_inside_contour(
881
- mean_shape, n_rows=args.grid_rows, n_cols=args.grid_cols)
908
+ if args.grid_ext > 0:
909
+ from homology_grid_ext import create_grid_extended
910
+ grid_info = create_grid_extended(mean_shape, n_rows=args.grid_rows, n_cols=args.grid_cols,
911
+ ext=args.grid_ext, min_overlap=args.min_cell_overlap)
912
+ else:
913
+ grid_info = create_grid_inside_contour(
914
+ mean_shape, n_rows=args.grid_rows, n_cols=args.grid_cols)
915
+ occluder_lookup, coverage = None, None
916
+ if args.occluder_categories.strip():
917
+ from homology_grid_ext import build_occluder_lookup
918
+ occluder_lookup = build_occluder_lookup(args.json, args.occluder_categories.split(','),
919
+ target=cat_name) # never the structure itself or its nested parts
920
+ coverage = {}
882
921
 
883
922
  ctrl_step = args.landmark_step
884
923
  n_ctrl = len(range(0, mean_shape.shape[0], ctrl_step))
@@ -898,7 +937,8 @@ def main():
898
937
  return process_specimen(fn, contour, mean_shape, grid_info,
899
938
  args.image_dir, mask_lookup,
900
939
  ctrl_step=ctrl_step,
901
- color_bins=color_bins)
940
+ color_bins=color_bins,
941
+ occluder_lookup=occluder_lookup, coverage_out=coverage)
902
942
 
903
943
  items = list(specimens.items())
904
944
  results = []
@@ -928,9 +968,14 @@ def main():
928
968
  viz_dir = os.path.join(args.output_dir, 'grid_overlays')
929
969
  os.makedirs(viz_dir, exist_ok=True)
930
970
  # v2: 6-tuple — use *_rest to stay robust if tuple ever grows.
931
- for fn, fv, img, mask, wi, *_rest in results[:args.viz_specimens]:
971
+ n_viz = len(results) if args.viz_specimens < 0 else args.viz_specimens
972
+ for fn, fv, img, mask, wi, *_rest in results[:n_viz]:
932
973
  viz_warped_grid_on_specimen(fn, img, mask, wi, grid_info, viz_dir, cat_name)
933
- logging.info(f"Saved {min(len(results), args.viz_specimens)} grid overlays")
974
+ logging.info(f"Saved {min(len(results), n_viz)} grid overlays")
975
+ if coverage is not None:
976
+ from homology_grid_ext import write_coverage_csv
977
+ write_coverage_csv(os.path.join(args.output_dir, f"color_homology_visible_fraction_{cat_name}.csv"), coverage, grid_info)
978
+ logging.info(f"Saved per-cell visible fraction for {len(coverage)} specimens")
934
979
 
935
980
  # --- Assemble feature matrix ---
936
981
  fns = [r[0] for r in results]