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