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