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