descriptron-core 2.1.3__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/PKG-INFO +1 -2
  2. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/pyproject.toml +1 -1
  3. descriptron_core-2.2.0/src/descriptron_core/tools/descriptron_phylo.py +377 -0
  4. descriptron_core-2.2.0/src/descriptron_core/tools/validate_phylo_published_waldron2025.py +116 -0
  5. descriptron_core-2.2.0/src/descriptron_core/tools/validate_phylo_real_tree.py +155 -0
  6. descriptron_core-2.2.0/src/descriptron_core/tools/validate_phylo_traits.py +186 -0
  7. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/validate_semilandmarks_vs_geomorph.py +5 -0
  8. descriptron_core-2.1.3/NOTICE.bak_pre_212_20260929_1100 +0 -59
  9. descriptron_core-2.1.3/pyproject.toml.bak_pre_203_20260924_145320 +0 -81
  10. descriptron_core-2.1.3/pyproject.toml.bak_pre_204_20260924_165335 +0 -82
  11. descriptron_core-2.1.3/pyproject.toml.bak_pre_210_20260925_103754 +0 -82
  12. descriptron_core-2.1.3/pyproject.toml.bak_pre_211_20260925_114912 +0 -82
  13. descriptron_core-2.1.3/pyproject.toml.bak_pre_213_20260929_1414 +0 -82
  14. descriptron_core-2.1.3/pyproject.toml.bak_pre_keys_20260924_165335 +0 -81
  15. descriptron_core-2.1.3/src/descriptron_core/cli.py.bak_pre_keys_20260924_165335 +0 -98
  16. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/.gitignore +0 -0
  17. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/LICENSE +0 -0
  18. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/NOTICE +0 -0
  19. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/README.md +0 -0
  20. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/__init__.py +0 -0
  21. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/cli.py +0 -0
  22. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/data/biorag_prompts/biorag_system_prompts_v1.txt +0 -0
  23. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/data/biorag_prompts/biorag_system_prompts_v2.txt +0 -0
  24. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/data/biorag_prompts/diaphorina_user_prompts.txt +0 -0
  25. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/data/biorag_prompts/ontologies/aism.obo +0 -0
  26. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/data/biorag_prompts/ontologies/pato.obo +0 -0
  27. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/SDD.xsd +0 -0
  28. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/SDD_EnumLib.xsd +0 -0
  29. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/SDD_TypeLib.xsd +0 -0
  30. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_BasePattern.xsd +0 -0
  31. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_CoreExtensions.xsd +0 -0
  32. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_CoreOntology.xsd +0 -0
  33. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_EnumLib.xsd +0 -0
  34. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_LinneanCore.xsd +0 -0
  35. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/UBIF_TypeLib.xsd +0 -0
  36. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/data/biorag_prompts/schemas/sdd_1.1/w3c-schemata/xml.xsd +0 -0
  37. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/data/biorag_prompts/schemas/taxpub/sample_treatment_deposit_example.xml +0 -0
  38. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/data/biorag_prompts/schemas/taxpub/sample_zookeys_26056_tp.xml +0 -0
  39. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/data/biorag_prompts/schemas/taxpub/tax-treatment-NS0-v1_flat.dtd +0 -0
  40. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/data/biorag_prompts/taxon_profiles/TEMPLATE_taxon_profile.yaml +0 -0
  41. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/data/biorag_prompts/taxon_profiles/diaphorina_taxon_profile.yaml +0 -0
  42. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/FHS_and_CLAHE_V21.py +0 -0
  43. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_add_discrete_characters_v1.py +0 -0
  44. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_annotation_reorient_v1.py +0 -0
  45. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_annotation_screen_v1.py +0 -0
  46. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_autapomorphy_v1.py +0 -0
  47. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_calibrate_v1.py +0 -0
  48. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_calibration_figures_v1.py +0 -0
  49. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_character_gate_v1.py +0 -0
  50. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_character_reliability_v1.py +0 -0
  51. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_character_robustness_v1.py +0 -0
  52. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_collaborator_forms_v1.py +0 -0
  53. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_computed_sets_v1.py +0 -0
  54. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_confabulation_checker_v2.py +0 -0
  55. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_congruence_compare_v1.py +0 -0
  56. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_description_refiner_v1.py +0 -0
  57. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_descriptive_scoring_v1.py +0 -0
  58. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_feature_policy.py +0 -0
  59. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_frame_orientation_calibrate_v1.py +0 -0
  60. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_frame_orientation_v1.py +0 -0
  61. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_gate_compare_v1.py +0 -0
  62. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_graph_identify_v1.py +0 -0
  63. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_graph_support_figure_v1.py +0 -0
  64. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_homology_frame_v1.py +0 -0
  65. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_instrument_compare_v3.py +0 -0
  66. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_key_builder_v1.py +0 -0
  67. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_key_feature_filter_v2.py +0 -0
  68. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_key_fuzzy_figures_v1.py +0 -0
  69. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_key_fuzzy_v1.py +0 -0
  70. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_key_holdout_v1.py +0 -0
  71. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_key_jackknife_support_v1.py +0 -0
  72. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_label_conflict_check_v1.py +0 -0
  73. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_llm_backend.py +0 -0
  74. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_make_taxon_profile_v1.py +0 -0
  75. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_methods_summary_figure_v1.py +0 -0
  76. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_novelty_score_v1.py +0 -0
  77. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_ontology_annotator_v2.py +0 -0
  78. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_outgroup_batch_check_v1.py +0 -0
  79. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_outline_shape_v1.py +0 -0
  80. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_prompts/biorag_system_prompts_v1.txt +0 -0
  81. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_prompts/biorag_system_prompts_v2.txt +0 -0
  82. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_prompts/diaphorina_user_prompts.txt +0 -0
  83. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_prompts/ontologies/aism.obo +0 -0
  84. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_prompts/ontologies/pato.obo +0 -0
  85. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/SDD.xsd +0 -0
  86. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/SDD_EnumLib.xsd +0 -0
  87. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/SDD_TypeLib.xsd +0 -0
  88. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_BasePattern.xsd +0 -0
  89. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_CoreExtensions.xsd +0 -0
  90. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_CoreOntology.xsd +0 -0
  91. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_EnumLib.xsd +0 -0
  92. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_LinneanCore.xsd +0 -0
  93. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/UBIF_TypeLib.xsd +0 -0
  94. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_prompts/schemas/sdd_1.1/w3c-schemata/xml.xsd +0 -0
  95. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_prompts/schemas/taxpub/sample_treatment_deposit_example.xml +0 -0
  96. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_prompts/schemas/taxpub/sample_zookeys_26056_tp.xml +0 -0
  97. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_prompts/schemas/taxpub/tax-treatment-NS0-v1_flat.dtd +0 -0
  98. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_prompts/taxon_profiles/TEMPLATE_taxon_profile.yaml +0 -0
  99. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_prompts/taxon_profiles/diaphorina_taxon_profile.yaml +0 -0
  100. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_provenance_v1.py +0 -0
  101. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_rebuild_downstream_v1.sh +0 -0
  102. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_reliability_figures_v1.py +0 -0
  103. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_rename_species_v1.py +0 -0
  104. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_rerun_descriptive_tables_v1.sh +0 -0
  105. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_retest_compare_v1.py +0 -0
  106. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_specimen_id.py +0 -0
  107. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_subjective_checks_v1.py +0 -0
  108. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_type_material_v1.py +0 -0
  109. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_vlm_characters_v1.py +0 -0
  110. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_vlm_heatmap_figures_v1.py +0 -0
  111. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_vlm_reliability_figure_v1.py +0 -0
  112. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biorag_vlm_roi_figures_v1.py +0 -0
  113. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biosyslit_rag_retrieval.py +0 -0
  114. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/biosyslit_rag_retrieval_v2.py +0 -0
  115. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/build_monograph_exports_v1.py +0 -0
  116. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/build_species_treatment_docx_v2.py +0 -0
  117. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/coco_combiner_V13.py +0 -0
  118. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/coco_converter_v24.py +0 -0
  119. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/color_extraction_with_names-v8.py +0 -0
  120. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/color_phenomics_homology_v2_1.py +0 -0
  121. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/compile_specimen_data.py +0 -0
  122. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/descriptron_centerline.py +0 -0
  123. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/descriptron_coco_tools.py +0 -0
  124. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/descriptron_convert.py +0 -0
  125. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/descriptron_credentials.py +0 -0
  126. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/descriptron_joints.py +0 -0
  127. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/descriptron_metadata.py +0 -0
  128. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/descriptron_rosetta.py +0 -0
  129. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/descriptron_shape_stats.py +0 -0
  130. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/landmark_gpa_V1.py +0 -0
  131. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/landmark_gpa_V2.py +0 -0
  132. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/make_reproducibility_bundle.py +0 -0
  133. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/run_full_pipeline_v2.py +0 -0
  134. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/semi_landmark_and_kpts_procrustesV34_GPA.py +0 -0
  135. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/semi_landmark_and_kpts_procrustesV42_GPA.py +0 -0
  136. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/texture_phenomics_from_coco.py +0 -0
  137. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/texture_phenomics_homology.py +0 -0
  138. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/validate_centerline_lengths.py +0 -0
  139. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/validate_pipeline_gm_vs_geomorph.py +0 -0
  140. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/validate_shape_stats_vs_geomorph.py +0 -0
  141. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/validate_shape_stats_vs_rrpp.py +0 -0
  142. {descriptron_core-2.1.3 → descriptron_core-2.2.0}/src/descriptron_core/tools/validate_texture_glcm.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: descriptron-core
3
- Version: 2.1.3
3
+ Version: 2.2.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
@@ -9,7 +9,6 @@ Author: Alex R. Van Dam
9
9
  License: Apache-2.0
10
10
  License-File: LICENSE
11
11
  License-File: NOTICE
12
- License-File: NOTICE.bak_pre_212_20260929_1100
13
12
  Keywords: COCO,morphometrics,phenomics,species description,taxonomy
14
13
  Classifier: Intended Audience :: Science/Research
15
14
  Classifier: License :: OSI Approved :: Apache Software License
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "descriptron-core"
7
- version = "2.1.3"
7
+ version = "2.2.0"
8
8
  description = "Evidence-tiered species descriptions, keys and audits from phenomic matrices"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -0,0 +1,377 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ descriptron_phylo.py - phylogenetic comparative analyses of any continuous Descriptron traits
4
+ =============================================================================================
5
+
6
+ Given a Newick tree and one or more trait tables (landmark shape, linear measurements, colour pattern, texture, or
7
+ any other continuous features), computes on species means:
8
+
9
+ signal multivariate phylogenetic signal Kmult (Adams 2014), permutation P and effect size Z,
10
+ as geomorph::physignal
11
+ pgls phylogenetic regression of a trait set on a predictor (for example log centroid size: phylogenetic
12
+ allometry) by generalised least squares under Brownian motion, SS / R2 / F with residual
13
+ randomisation, as geomorph::procD.pgls
14
+ morphospace principal components of the species means with ancestral states estimated under Brownian motion
15
+ (generalised least squares) projected into the same space and the tree drawn through them, as
16
+ geomorph::gm.prcomp(phy = )
17
+
18
+ Trait tables: CSV with one row per specimen (or per species) and numeric columns. The species of each row comes
19
+ from a column (--species_col) or from a regular expression applied to an id column (--species_regex). Rows of the
20
+ same species are averaged. Species absent from the tree, or tree tips without data, are dropped (the tree is pruned).
21
+
22
+ python descriptron_phylo.py --tree tree.nwk --out_dir phylo/ \
23
+ --traits shape=landmark_gpa/forewing/forewing_procrustes_coords.csv \
24
+ --traits colour=color_homology/whole_wing/color_homology_features_whole_wing.csv \
25
+ --species_regex "morph_([A-Za-z]+\\d*?)_?\\d+_" --scale colour=standardize \
26
+ --pgls shape~logCS --analyses signal pgls morphospace
27
+
28
+ Scaling (--scale NAME=none|standardize|log): Kmult and the PGLS statistics weight every column by its variance, so a
29
+ set that mixes units (millimetres with ratios, colour channels with entropies) should be standardised; Procrustes
30
+ shape coordinates are analysed as they are (the default for a set whose columns look like x/y coordinates).
31
+
32
+ Validated against geomorph 4.1.1 and ape on geomorph's plethspecies data (validation/validate_phylo_real_tree.py).
33
+ """
34
+ from __future__ import annotations
35
+
36
+ import argparse
37
+ import csv
38
+ import json
39
+ import re
40
+ import sys
41
+ from pathlib import Path
42
+ from typing import Dict, List, Optional, Tuple
43
+
44
+ import numpy as np
45
+
46
+ # ============================================================================ tree
47
+ class Node:
48
+ __slots__ = ("name", "length", "kids", "parent", "depth", "idx")
49
+
50
+ def __init__(self, name="", length=0.0):
51
+ self.name, self.length, self.kids, self.parent, self.depth, self.idx = name, length, [], None, 0.0, -1
52
+
53
+
54
+ def parse_newick(s: str) -> Node:
55
+ s = s.strip().rstrip(";")
56
+ pos = 0
57
+
58
+ def node() -> Node:
59
+ nonlocal pos
60
+ n = Node()
61
+ if s[pos] == "(":
62
+ pos += 1
63
+ n.kids.append(node())
64
+ while s[pos] == ",":
65
+ pos += 1
66
+ n.kids.append(node())
67
+ pos += 1 # ')'
68
+ m = re.match(r"([^:,();\[]*)(?:\[[^\]]*\])?(?::([0-9.eE+\-]+))?", s[pos:])
69
+ pos += m.end()
70
+ n.name = m.group(1).strip().strip("'\"")
71
+ n.length = float(m.group(2) or 0.0)
72
+ for k in n.kids:
73
+ k.parent = n
74
+ return n
75
+
76
+ root = node()
77
+ root.length = 0.0 # the root's own branch does not count
78
+ _set_depths(root)
79
+ return root
80
+
81
+
82
+ def _set_depths(n: Node, d: float = 0.0):
83
+ n.depth = d
84
+ for k in n.kids:
85
+ _set_depths(k, d + k.length)
86
+
87
+
88
+ def tips_of(n: Node) -> List[Node]:
89
+ return [n] if not n.kids else [t for k in n.kids for t in tips_of(k)]
90
+
91
+
92
+ def internal_nodes(n: Node) -> List[Node]:
93
+ return ([n] if n.kids else []) + [x for k in n.kids for x in internal_nodes(k)]
94
+
95
+
96
+ def prune(root: Node, keep: set) -> Node:
97
+ """Drop tips not in `keep`; collapse nodes left with one child (their branch lengths add up)."""
98
+ def rec(n: Node) -> Optional[Node]:
99
+ if not n.kids:
100
+ return n if n.name in keep else None
101
+ kids = [c for c in (rec(k) for k in n.kids) if c is not None]
102
+ if not kids:
103
+ return None
104
+ if len(kids) == 1:
105
+ kids[0].length += n.length
106
+ return kids[0]
107
+ n.kids = kids
108
+ for k in kids:
109
+ k.parent = n
110
+ return n
111
+
112
+ r = rec(root)
113
+ r.length, r.parent = 0.0, None
114
+ _set_depths(r)
115
+ return r
116
+
117
+
118
+ def mrca_depth(a: Node, b: Node) -> float:
119
+ anc = set()
120
+ x = a
121
+ while x is not None:
122
+ anc.add(id(x)); x = x.parent
123
+ y = b
124
+ while id(y) not in anc:
125
+ y = y.parent
126
+ return y.depth
127
+
128
+
129
+ def vcv(nodes_a: List[Node], nodes_b: List[Node]) -> np.ndarray:
130
+ """Brownian-motion covariance: shared path length from the root (tips and internal nodes alike)."""
131
+ return np.array([[mrca_depth(a, b) for b in nodes_b] for a in nodes_a])
132
+
133
+
134
+ # ============================================================================ statistics
135
+ def _whiten(C: np.ndarray) -> np.ndarray:
136
+ """P with P'P = C^-1 (any such P gives the same GLS sums of squares)."""
137
+ return np.linalg.inv(np.linalg.cholesky(C))
138
+
139
+
140
+ def kmult(Y: np.ndarray, C: np.ndarray) -> float:
141
+ """Adams (2014) multivariate K, as geomorph::physignal."""
142
+ n = len(Y)
143
+ Ci = np.linalg.inv(C)
144
+ one = np.ones((n, 1))
145
+ a = np.linalg.solve(one.T @ Ci @ one, one.T @ Ci @ Y)
146
+ R = Y - one @ a
147
+ obs = np.trace(R.T @ R) / np.trace(R.T @ Ci @ R)
148
+ expct = (np.trace(C) - n / Ci.sum()) / (n - 1)
149
+ return float(obs / expct)
150
+
151
+
152
+ def effect_size(obs: float, rand: np.ndarray) -> float:
153
+ r = np.r_[obs, rand]
154
+ sd = r.std(ddof=1)
155
+ return float((obs - r.mean()) / sd) if sd > 0 else float("nan")
156
+
157
+
158
+ def phylo_signal(Y, C, iters, rng) -> Dict:
159
+ K = kmult(Y, C)
160
+ rand = np.array([kmult(Y[rng.permutation(len(Y))], C) for _ in range(iters)])
161
+ return {"Kmult": K, "P": float((1 + (rand >= K - 1e-12).sum()) / (iters + 1)), "Z": effect_size(K, rand)}
162
+
163
+
164
+ def pgls(Y: np.ndarray, x: np.ndarray, C: np.ndarray, iters: int, rng) -> Dict:
165
+ """Y ~ x under Brownian motion by GLS; SS from the whitened data, residual randomisation of the null model
166
+ (intercept only), as RRPP / geomorph::procD.pgls with a single predictor."""
167
+ n = len(Y)
168
+ P = _whiten(C)
169
+ Yt = P @ Y
170
+ X0 = P @ np.ones((n, 1))
171
+ X1 = P @ np.column_stack([np.ones(n), x])
172
+
173
+ def rss(X, Yy):
174
+ B, *_ = np.linalg.lstsq(X, Yy, rcond=None)
175
+ R = Yy - X @ B
176
+ return float((R ** 2).sum())
177
+
178
+ B0, *_ = np.linalg.lstsq(X0, Yt, rcond=None)
179
+ fit0, res0 = X0 @ B0, Yt - X0 @ B0
180
+ ss_tot, ss_res = rss(X0, Yt), rss(X1, Yt)
181
+ ss = ss_tot - ss_res
182
+ df, dfr = 1, n - 2
183
+ F = (ss / df) / (ss_res / dfr)
184
+ rand = []
185
+ for _ in range(iters):
186
+ Yp = fit0 + res0[rng.permutation(n)]
187
+ s_t, s_r = rss(X0, Yp), rss(X1, Yp)
188
+ rand.append(((s_t - s_r) / df) / (s_r / dfr))
189
+ rand = np.array(rand)
190
+ return {"Df": df, "SS": ss, "MS": ss / df, "Rsq": ss / ss_tot, "F": F, "Z": effect_size(F, rand),
191
+ "P": float((1 + (rand >= F - 1e-12).sum()) / (iters + 1)), "SS_residual": ss_res, "SS_total": ss_tot}
192
+
193
+
194
+ def ancestral_states(Y: np.ndarray, tips: List[Node], nodes: List[Node]) -> np.ndarray:
195
+ """Ancestral states under Brownian motion by GLS: a = mu + C_nt C_tt^-1 (Y - 1 mu), with mu the GLS root state."""
196
+ Ctt = vcv(tips, tips)
197
+ Cnt = vcv(nodes, tips)
198
+ Ci = np.linalg.inv(Ctt)
199
+ one = np.ones((len(tips), 1))
200
+ mu = np.linalg.solve(one.T @ Ci @ one, one.T @ Ci @ Y)
201
+ return mu + Cnt @ Ci @ (Y - one @ mu)
202
+
203
+
204
+ # ============================================================================ data
205
+ def _is_number(v: str) -> bool:
206
+ try:
207
+ float(v); return True
208
+ except ValueError:
209
+ return False
210
+
211
+
212
+ def load_traits(path: str, species_col: Optional[str], species_regex: Optional[str], id_col: Optional[str]):
213
+ rows = list(csv.DictReader(open(path, newline="")))
214
+ if not rows:
215
+ sys.exit(f"{path}: no rows")
216
+ cols = list(rows[0])
217
+ if species_col and species_col in cols:
218
+ sp = [r[species_col] for r in rows]
219
+ else:
220
+ idc = id_col or next((c for c in ("filename", "specimen_id", "specimen", "image", "id", "file") if c in cols), cols[0])
221
+ if not species_regex:
222
+ sys.exit(f"{path}: give --species_col or --species_regex (no species column found)")
223
+ rx = re.compile(species_regex)
224
+ sp = []
225
+ for r in rows:
226
+ m = rx.search(r[idc])
227
+ sp.append(m.group(1) if m else None)
228
+ num = [c for c in cols if c != species_col and all(_is_number(r[c]) or r[c] == "" for r in rows)
229
+ and any(r[c] != "" for r in rows)]
230
+ M = np.array([[float(r[c]) if r[c] != "" else np.nan for c in num] for r in rows])
231
+ by: Dict[str, List[int]] = {}
232
+ for i, s in enumerate(sp):
233
+ if s:
234
+ by.setdefault(s, []).append(i)
235
+ species = sorted(by)
236
+ means = np.array([np.nanmean(M[by[s]], axis=0) for s in species])
237
+ keep = ~np.isnan(means).any(axis=0) # drop columns missing for any species
238
+ return species, [c for c, k in zip(num, keep) if k], means[:, keep], {s: len(v) for s, v in by.items()}
239
+
240
+
241
+ def scale(M: np.ndarray, how: str) -> np.ndarray:
242
+ if how == "standardize":
243
+ sd = M.std(axis=0, ddof=1)
244
+ ok = sd > 0
245
+ return (M[:, ok] - M[:, ok].mean(0)) / sd[ok]
246
+ if how == "log":
247
+ if (M <= 0).any():
248
+ sys.exit("--scale log needs positive values")
249
+ return np.log(M)
250
+ return M
251
+
252
+
253
+ # ============================================================================ plot
254
+ def plot_morphospace(pc_tips, pc_nodes, tips, nodes, ev, path, title):
255
+ import matplotlib
256
+ matplotlib.use("Agg")
257
+ import matplotlib.pyplot as plt
258
+ pos = {id(t): pc_tips[i] for i, t in enumerate(tips)}
259
+ pos.update({id(n): pc_nodes[i] for i, n in enumerate(nodes)})
260
+ fig, ax = plt.subplots(figsize=(6.5, 5.5))
261
+ for n in nodes:
262
+ for k in n.kids:
263
+ a, b = pos[id(n)], pos[id(k)]
264
+ ax.plot([a[0], b[0]], [a[1], b[1]], color="#6b7780", lw=0.9, zorder=1)
265
+ ax.scatter(pc_nodes[:, 0], pc_nodes[:, 1], s=14, color="white", edgecolor="#6b7780", zorder=2, label="ancestral states")
266
+ ax.scatter(pc_tips[:, 0], pc_tips[:, 1], s=30, color="#0072B2", zorder=3, label="species means")
267
+ for i, t in enumerate(tips):
268
+ ax.annotate(t.name, pc_tips[i, :2], xytext=(3, 3), textcoords="offset points", fontsize=7)
269
+ ax.set_xlabel(f"PC1 ({100 * ev[0]:.1f}%)"); ax.set_ylabel(f"PC2 ({100 * ev[1]:.1f}%)")
270
+ ax.set_title(title, fontsize=10); ax.legend(fontsize=7, frameon=False)
271
+ fig.tight_layout(); fig.savefig(path, dpi=200); plt.close(fig)
272
+
273
+
274
+ # ============================================================================ main
275
+ def main(argv=None):
276
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
277
+ ap.add_argument("--tree", required=True, help="Newick tree whose tips are species names")
278
+ ap.add_argument("--traits", action="append", required=True, metavar="NAME=CSV",
279
+ help="a trait table; repeat for several sets (shape, measurements, colour, texture, ...)")
280
+ ap.add_argument("--species_col", default=None, help="column holding the species (default: use --species_regex)")
281
+ ap.add_argument("--species_regex", default=None, help="regex with one group extracting the species from the id column")
282
+ ap.add_argument("--id_col", default=None, help="id column for --species_regex (default: filename/specimen_id/...)")
283
+ ap.add_argument("--columns", action="append", default=[], metavar="NAME=REGEX",
284
+ help="keep only the columns of trait set NAME that match REGEX")
285
+ ap.add_argument("--scale", action="append", default=[], metavar="NAME=none|standardize|log")
286
+ ap.add_argument("--size_col", default=r"centroid|csize",
287
+ help="regex for a size column kept out of the trait set and used as the logCS predictor")
288
+ ap.add_argument("--pgls", action="append", default=[], metavar="SET~PREDICTOR",
289
+ help="PREDICTOR is logCS (from a set's centroid-size column) or SET2:COLUMN")
290
+ ap.add_argument("--analyses", nargs="+", default=["signal", "morphospace"], choices=["signal", "pgls", "morphospace"])
291
+ ap.add_argument("--iterations", type=int, default=999); ap.add_argument("--seed", type=int, default=1)
292
+ ap.add_argument("--out_dir", required=True)
293
+ a = ap.parse_args(argv)
294
+ out = Path(a.out_dir); out.mkdir(parents=True, exist_ok=True)
295
+ rng = np.random.default_rng(a.seed)
296
+ root = parse_newick(Path(a.tree).read_text())
297
+ colsel = dict(x.split("=", 1) for x in a.columns)
298
+ scales = dict(x.split("=", 1) for x in a.scale)
299
+
300
+ sets = {}
301
+ for spec in a.traits:
302
+ name, path = spec.split("=", 1)
303
+ sp, cols, M, counts = load_traits(path, a.species_col, a.species_regex, a.id_col)
304
+ if name in colsel:
305
+ rx = re.compile(colsel[name]); k = [i for i, c in enumerate(cols) if rx.search(c)]
306
+ cols, M = [cols[i] for i in k], M[:, k]
307
+ # a size column (centroid size) is kept aside: it is a predictor (logCS), not part of the trait set
308
+ szi = [i for i, c in enumerate(cols) if re.search(a.size_col, c, re.I)]
309
+ size = M[:, szi[0]] if szi else None
310
+ keep = [i for i in range(len(cols)) if i not in szi]
311
+ sets[name] = {"species": sp, "cols": [cols[i] for i in keep], "raw": M[:, keep], "size": size,
312
+ "counts": counts}
313
+
314
+ report = ["# Phylogenetic comparative analyses", "", f"Tree: {a.tree}", ""]
315
+ signal_rows, pgls_rows = [], []
316
+ for name, S in sets.items():
317
+ tipnames = {t.name for t in tips_of(root)}
318
+ common = [s for s in S["species"] if s in tipnames]
319
+ if len(common) < 4:
320
+ report.append(f"{name}: only {len(common)} species match the tree - skipped"); continue
321
+ tree = prune(parse_newick(Path(a.tree).read_text()), set(common))
322
+ tips = tips_of(tree)
323
+ idx = [S["species"].index(t.name) for t in tips]
324
+ how = scales.get(name, "none")
325
+ Y = scale(S["raw"][idx], how)
326
+ C = vcv(tips, tips)
327
+ S.update(tree=tree, tips=tips, Y=Y, C=C, idx=idx)
328
+ dropped = sorted(set(S["species"]) - set(common))
329
+ report += [f"## {name}", f"{len(tips)} species, {Y.shape[1]} variables (scale: {how})"
330
+ + (f"; not in the tree: {', '.join(dropped)}" if dropped else ""), ""]
331
+ if "signal" in a.analyses:
332
+ r = phylo_signal(Y, C, a.iterations, rng)
333
+ signal_rows.append({"trait_set": name, "n_species": len(tips), "n_variables": Y.shape[1], "scale": how, **r})
334
+ report.append(f"Phylogenetic signal: Kmult = {r['Kmult']:.4f}, P = {r['P']:.4f}, Z = {r['Z']:.2f}")
335
+ if "morphospace" in a.analyses:
336
+ nodes = internal_nodes(tree)
337
+ anc = ancestral_states(Y, tips, nodes)
338
+ mu = Y.mean(0)
339
+ U, sv, Vt = np.linalg.svd(Y - mu, full_matrices=False)
340
+ ev = sv ** 2 / (sv ** 2).sum()
341
+ pc_t, pc_n = (Y - mu) @ Vt.T, (anc - mu) @ Vt.T
342
+ np.savetxt(out / f"{name}_morphospace_tips.csv", pc_t[:, :min(5, pc_t.shape[1])], delimiter=",",
343
+ header=",".join(f"PC{i + 1}" for i in range(min(5, pc_t.shape[1]))), comments="")
344
+ plot_morphospace(pc_t, pc_n, tips, nodes, ev, out / f"{name}_phylomorphospace.png",
345
+ f"{name}: phylomorphospace ({len(tips)} species)")
346
+ report.append(f"Phylomorphospace: {name}_phylomorphospace.png (PC1 {100 * ev[0]:.1f}%, PC2 {100 * ev[1]:.1f}%)")
347
+ report.append("")
348
+
349
+ if "pgls" in a.analyses:
350
+ for f in a.pgls:
351
+ ys, pred = [x.strip() for x in f.split("~")]
352
+ S = sets.get(ys)
353
+ if S is None or "Y" not in S:
354
+ report.append(f"PGLS {f}: trait set {ys} not analysed - skipped"); continue
355
+ if pred == "logCS":
356
+ if S["size"] is None:
357
+ report.append(f"PGLS {f}: no size column in {ys} (--size_col) - skipped"); continue
358
+ xv = np.log(np.array([S["size"][S["species"].index(t.name)] for t in S["tips"]]))
359
+ else:
360
+ s2, cc = pred.split(":", 1); src = sets.get(s2)
361
+ if src is None or cc not in src["cols"]:
362
+ report.append(f"PGLS {f}: predictor column not found - skipped"); continue
363
+ xv = np.array([src["raw"][src["species"].index(t.name), src["cols"].index(cc)] for t in S["tips"]])
364
+ r = pgls(S["Y"], xv, S["C"], a.iterations, rng)
365
+ pgls_rows.append({"model": f, **r})
366
+ report.append(f"PGLS {f}: R2 = {r['Rsq']:.4f}, F = {r['F']:.3f}, P = {r['P']:.4f}")
367
+
368
+ for fn, rows in (("phylogenetic_signal.csv", signal_rows), ("pgls.csv", pgls_rows)):
369
+ if rows:
370
+ with open(out / fn, "w", newline="") as fh:
371
+ w = csv.DictWriter(fh, fieldnames=list(rows[0])); w.writeheader(); w.writerows(rows)
372
+ (out / "phylo_report.md").write_text("\n".join(report) + "\n")
373
+ print("\n".join(report))
374
+
375
+
376
+ if __name__ == "__main__":
377
+ main()
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env python3
2
+ """Reproduce published phylogenetic-signal results with Descriptron: Waldron et al. (2025, Evolution 79: 2369;
3
+ doi:10.1093/evolut/qpaf167), 57 Plethodon species, data and trees on Dryad (doi:10.5061/dryad.bvq83bknf).
4
+
5
+ Their Table S5 gives Blomberg's K (and Pagel's lambda) for six traits on the pruned MCMCtree timetree: costal-groove
6
+ count (CG) and five body-size-corrected measurements (divided by SVL, log-transformed: S.E, HL, BW, FLL, HLL). Here the
7
+ same K is computed by Descriptron (descriptron_phylo.kmult on one trait = Blomberg's K), phytools::phylosig and
8
+ geomorph::physignal, all on their processed data and their tree, and compared with the published values. Their standard
9
+ PCA (PC1 83.4%, PC2 6.5% in the SI) is recomputed as well. Pagel's lambda is not implemented in Descriptron.
10
+
11
+ Download the Dryad files by hand (Dryad blocks scripted downloads) and unzip them into one folder:
12
+
13
+ python validate_phylo_published_waldron2025.py --data_dir <folder> --rscript /path/Rscript --rlib /path/Rlib \
14
+ --out-dir waldron_check/
15
+ """
16
+ import argparse, csv, importlib.util, json, subprocess
17
+ from pathlib import Path
18
+ import numpy as np
19
+
20
+
21
+ def _sibling(name):
22
+ """measure/<name> in the source tree; beside this file once packaged (tools/)."""
23
+ here = Path(__file__).resolve()
24
+ return next((c for c in (here.parents[1] / name, here.parent / name) if c.exists()), here.parents[1] / name)
25
+
26
+ TRAITS = ["CG", "S.E", "HL", "BW", "FLL", "HLL"]
27
+
28
+ R_CODE = r'''
29
+ args <- commandArgs(TRUE); .libPaths(c(args[1], .libPaths())); d <- args[2]
30
+ suppressMessages({library(ape); library(geomorph); library(phytools)})
31
+ phy <- read.tree(file.path(d, "tree.nwk"))
32
+ X <- read.csv(file.path(d, "traits.csv"), row.names = 1, check.names = FALSE)
33
+ X <- X[phy$tip.label, , drop = FALSE]
34
+ if (anyNA(X)) { # as the authors did: phylogenetic imputation with Rphylopars (Brownian motion, defaults)
35
+ suppressMessages(library(Rphylopars))
36
+ td <- data.frame(species = rownames(X), X, check.names = FALSE)
37
+ pr <- phylopars(trait_data = td, tree = phy)
38
+ imp <- pr$anc_recon[rownames(X), colnames(X), drop = FALSE]
39
+ X[is.na(X)] <- imp[is.na(X)]
40
+ }
41
+ write.csv(X, file.path(d, "traits_used.csv"))
42
+ out <- data.frame()
43
+ for (v in colnames(X)) {
44
+ x <- X[[v]]; names(x) <- rownames(X)
45
+ set.seed(1); k <- phylosig(phy, x, method = "K", test = TRUE, nsim = 999)
46
+ set.seed(1); p <- physignal(matrix(x, ncol = 1, dimnames = list(names(x), v)), phy, iter = 999, print.progress = FALSE)
47
+ out <- rbind(out, data.frame(trait = v, K_phytools = k$K, P_phytools = k$P, K_geomorph = p$phy.signal, P_geomorph = p$pvalue))
48
+ }
49
+ write.csv(out, file.path(d, "R_K.csv"), row.names = FALSE)
50
+ '''
51
+
52
+
53
+ def main(argv=None):
54
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
55
+ ap.add_argument("--data_dir", required=True); ap.add_argument("--rscript", required=True)
56
+ ap.add_argument("--rlib", required=True); ap.add_argument("--out-dir", required=True)
57
+ ap.add_argument("--iter", type=int, default=999)
58
+ a = ap.parse_args(argv)
59
+ import pandas as pd
60
+ D, out = Path(a.data_dir), Path(a.out_dir); out.mkdir(parents=True, exist_ok=True)
61
+ morph = pd.read_csv(next(D.rglob("Plethodon_morphology_R_Processed*.csv")))
62
+ tree_txt = next(D.rglob("plethodon-pruned.tree")).read_text()
63
+ pub = pd.read_excel(next(D.rglob("Table_S5_Plethodon_phylosignal.xlsx")), header=None)
64
+ hdr = pub.index[pub[0].astype(str).eq("variable")][0]
65
+ pub = pub.iloc[hdr + 1:, :5]; pub.columns = ["trait", "lambda", "p_lambda", "K", "p_K"]
66
+ pub = pub.dropna(subset=["trait"]).set_index("trait").astype(float)
67
+
68
+ spec = importlib.util.spec_from_file_location("dp", _sibling("descriptron_phylo.py"))
69
+ dp = importlib.util.module_from_spec(spec); spec.loader.exec_module(dp)
70
+ tree = dp.parse_newick(tree_txt)
71
+ tips = dp.tips_of(tree); tn = [t.name for t in tips]
72
+ col = {"CG": "CG", "S.E": "S.E.sc", "HL": "HL.sc", "BW": "BW.sc", "FLL": "FLL.sc", "HLL": "HLL.sc"}
73
+ M = morph.set_index("Species").loc[tn, [col[t] for t in TRAITS]].astype(float)
74
+ M.columns = TRAITS
75
+ missing = [(i, c) for c in M.columns for i in M.index[M[c].isna()]]
76
+ (out / "tree.nwk").write_text(tree_txt)
77
+ M.to_csv(out / "traits.csv")
78
+ (out / "compare.R").write_text(R_CODE)
79
+ subprocess.run([a.rscript, str(out / "compare.R"), a.rlib, str(out)], check=True)
80
+ R = pd.read_csv(out / "R_K.csv").set_index("trait")
81
+ Mi = pd.read_csv(out / "traits_used.csv", index_col=0).loc[tn, TRAITS]
82
+ assert np.allclose(Mi.values[~M.isna().values], M.values[~M.isna().values]) # only the gaps were filled
83
+ M = Mi
84
+
85
+ C = dp.vcv(tips, tips)
86
+ rows = []
87
+ for t in TRAITS:
88
+ y = M[t].values[:, None]
89
+ r = dp.phylo_signal(y, C, a.iter, np.random.default_rng(1))
90
+ rows.append({"trait": t, "K_published": pub.loc[t, "K"], "K_descriptron": r["Kmult"],
91
+ "K_phytools": R.loc[t, "K_phytools"], "K_geomorph": R.loc[t, "K_geomorph"],
92
+ "diff_descriptron_vs_published": abs(r["Kmult"] - pub.loc[t, "K"]),
93
+ "rel_diff_descriptron_vs_phytools": abs(r["Kmult"] - R.loc[t, "K_phytools"]) / R.loc[t, "K_phytools"],
94
+ "P_published": pub.loc[t, "p_K"], "P_descriptron": r["P"],
95
+ "P_phytools": R.loc[t, "P_phytools"], "P_geomorph": R.loc[t, "P_geomorph"],
96
+ "lambda_published": pub.loc[t, "lambda"]})
97
+ res = pd.DataFrame(rows)
98
+ res.to_csv(out / "waldron2025_K_comparison.csv", index=False)
99
+ # their standard (non-phylogenetic) PCA: PC1 83.4%, PC2 6.5%
100
+ pcs = {}
101
+ for how in ("covariance", "correlation"):
102
+ Z = M.values - M.values.mean(0)
103
+ if how == "correlation":
104
+ Z = Z / M.values.std(0, ddof=1)
105
+ sv = np.linalg.svd(Z, compute_uv=False); ev = sv ** 2 / (sv ** 2).sum()
106
+ pcs[how] = [round(float(ev[0]) * 100, 1), round(float(ev[1]) * 100, 1)]
107
+ Mm = M.drop(columns=["CG"])
108
+ sv = np.linalg.svd(Mm.values - Mm.values.mean(0), compute_uv=False); ev = sv ** 2 / (sv ** 2).sum()
109
+ pcs["covariance, size-corrected traits only"] = [round(float(ev[0]) * 100, 1), round(float(ev[1]) * 100, 1)]
110
+ summary = {"species": len(tn), "imputed_cells": [f"{i}:{c}" for i, c in missing], "K": rows, "pca_percent_PC1_PC2": pcs, "pca_published": [83.4, 6.5]}
111
+ json.dump(summary, open(out / "waldron2025_summary.json", "w"), indent=2, default=float)
112
+ print(res.round(6).to_string()); print("PCA:", pcs, "published [83.4, 6.5]")
113
+
114
+
115
+ if __name__ == "__main__":
116
+ main()