hypertopos 0.7.2__tar.gz → 0.7.3__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 (336) hide show
  1. {hypertopos-0.7.2 → hypertopos-0.7.3}/CHANGELOG.md +22 -0
  2. {hypertopos-0.7.2 → hypertopos-0.7.3}/LICENSE.md +1 -1
  3. {hypertopos-0.7.2 → hypertopos-0.7.3}/PKG-INFO +1 -1
  4. {hypertopos-0.7.2 → hypertopos-0.7.3}/README.md +1 -1
  5. {hypertopos-0.7.2 → hypertopos-0.7.3}/ROADMAP.md +3 -0
  6. {hypertopos-0.7.2 → hypertopos-0.7.3}/SECURITY.md +1 -1
  7. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/__init__.py +1 -1
  8. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/builder/builder.py +134 -5
  9. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/builder/derived.py +30 -6
  10. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/aggregation.py +113 -0
  11. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/geometry.py +56 -0
  12. hypertopos-0.7.3/hypertopos/engine/graph.py +285 -0
  13. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/topology.py +182 -0
  14. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/model/sphere.py +58 -2
  15. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/navigation/navigator.py +858 -20
  16. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/storage/calibration_history.py +80 -0
  17. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/storage/reader.py +17 -3
  18. {hypertopos-0.7.2 → hypertopos-0.7.3}/pyproject.toml +1 -1
  19. hypertopos-0.7.3/tests/test_chain_signed_confidence_rollup.py +304 -0
  20. hypertopos-0.7.3/tests/test_cluster_a_label_aware_depth.py +416 -0
  21. hypertopos-0.7.3/tests/test_cluster_c1_sphere_overview_ergonomics.py +476 -0
  22. hypertopos-0.7.3/tests/test_cluster_c3_detector_closures.py +217 -0
  23. hypertopos-0.7.3/tests/test_cluster_d1_graph_phase3.py +315 -0
  24. hypertopos-0.7.3/tests/test_cluster_d2_influencer_auto_discovery.py +346 -0
  25. hypertopos-0.7.3/tests/test_local_trajectory_shape.py +88 -0
  26. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_navigation.py +110 -0
  27. {hypertopos-0.7.2 → hypertopos-0.7.3}/.github/workflows/publish-pypi.yml +0 -0
  28. {hypertopos-0.7.2 → hypertopos-0.7.3}/.gitignore +0 -0
  29. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/README.md +0 -0
  30. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/api-reference.md +0 -0
  31. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/architecture.md +0 -0
  32. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/LICENSE +0 -0
  33. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/README.md +0 -0
  34. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/README.md +0 -0
  35. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/SUMMARY.md +0 -0
  36. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-exp-a-report.md +0 -0
  37. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-exp-a-scoring.md +0 -0
  38. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-exp-b-report.md +0 -0
  39. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-exp-b-scoring.md +0 -0
  40. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-settings.md +0 -0
  41. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-exp-a-report.md +0 -0
  42. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-exp-a-scoring.md +0 -0
  43. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-exp-b-report.md +0 -0
  44. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-exp-b-scoring.md +0 -0
  45. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-settings.md +0 -0
  46. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-exp-a-report.md +0 -0
  47. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-exp-a-scoring.md +0 -0
  48. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-exp-b-report.md +0 -0
  49. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-exp-b-scoring.md +0 -0
  50. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-settings.md +0 -0
  51. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-exp-a-report.md +0 -0
  52. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-exp-a-scoring.md +0 -0
  53. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-exp-b-report.md +0 -0
  54. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-exp-b-scoring.md +0 -0
  55. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-settings.md +0 -0
  56. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-exp-a-report.md +0 -0
  57. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-exp-a-scoring.md +0 -0
  58. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-exp-b-report.md +0 -0
  59. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-exp-b-scoring.md +0 -0
  60. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-settings.md +0 -0
  61. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-exp-a-report.md +0 -0
  62. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-exp-a-scoring.md +0 -0
  63. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-exp-b-report.md +0 -0
  64. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-exp-b-scoring.md +0 -0
  65. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-settings.md +0 -0
  66. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/sphere/prepare_accounts.py +0 -0
  67. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/sphere/prepare_cpty_banks.py +0 -0
  68. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/sphere/prepare_districts.py +0 -0
  69. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/sphere/prepare_loan_accounts.py +0 -0
  70. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/sphere/prepare_operations.py +0 -0
  71. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/sphere/prepare_orders.py +0 -0
  72. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/sphere/prepare_transactions.py +0 -0
  73. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/sphere/prepare_tx_types.py +0 -0
  74. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/berka/sphere/sphere.yaml +0 -0
  75. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/ibm-aml/README.md +0 -0
  76. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/ibm-aml/_layer2_cross_eval.py +0 -0
  77. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/ibm-aml/_layer3_cross_eval.py +0 -0
  78. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_accounts.py +0 -0
  79. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_currencies.py +0 -0
  80. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_payment_formats.py +0 -0
  81. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_transactions.py +0 -0
  82. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/ibm-aml/hi_small_sphere/sphere.yaml +0 -0
  83. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/ibm-aml/layer1_passive_scan.py +0 -0
  84. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/ibm-aml/layer2_xgboost_sweep.py +0 -0
  85. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/ibm-aml/layer3_typology_filter.py +0 -0
  86. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/ibm-aml/li_small_sphere/prepare_accounts.py +0 -0
  87. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/ibm-aml/li_small_sphere/prepare_currencies.py +0 -0
  88. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/ibm-aml/li_small_sphere/prepare_payment_formats.py +0 -0
  89. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/ibm-aml/li_small_sphere/prepare_transactions.py +0 -0
  90. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/ibm-aml/li_small_sphere/sphere.yaml +0 -0
  91. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/ibm-aml/passive-scan-calibration.md +0 -0
  92. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/nyc-taxi/README.md +0 -0
  93. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/nyc-taxi/results/0.1.0/SUMMARY.md +0 -0
  94. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-a-report.md +0 -0
  95. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-a-scoring.md +0 -0
  96. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-b-report.md +0 -0
  97. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-b-scoring.md +0 -0
  98. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-c-report.md +0 -0
  99. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-settings.md +0 -0
  100. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-a-report.md +0 -0
  101. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-a-scoring.md +0 -0
  102. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-b-report.md +0 -0
  103. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-b-scoring.md +0 -0
  104. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-c-report.md +0 -0
  105. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-settings.md +0 -0
  106. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-a-report.md +0 -0
  107. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-a-scoring.md +0 -0
  108. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-b-report.md +0 -0
  109. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-b-scoring.md +0 -0
  110. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-c-report.md +0 -0
  111. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-settings.md +0 -0
  112. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/nyc-taxi/sphere/prepare_trips.py +0 -0
  113. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmark/nyc-taxi/sphere/sphere.yaml +0 -0
  114. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/benchmarks.md +0 -0
  115. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/concepts.md +0 -0
  116. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/configuration.md +0 -0
  117. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/data-format.md +0 -0
  118. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/external-chains-as-anchor-line.md +0 -0
  119. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/images/build-pipeline.svg +0 -0
  120. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/images/core-objects.svg +0 -0
  121. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/images/cutting-plane.svg +0 -0
  122. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/images/directory-layout.svg +0 -0
  123. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/images/hypertopos-overview.svg +0 -0
  124. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/images/model-lifecycle.svg +0 -0
  125. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/images/navigation-primitives.svg +0 -0
  126. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/images/three-scales.svg +0 -0
  127. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/introduction.md +0 -0
  128. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/patent-implementation-map.md +0 -0
  129. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/quickstart.md +0 -0
  130. {hypertopos-0.7.2 → hypertopos-0.7.3}/docs/refutations.md +0 -0
  131. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/builder/__init__.py +0 -0
  132. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/builder/_bootstrap.py +0 -0
  133. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/builder/_bregman.py +0 -0
  134. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/builder/_scatter.py +0 -0
  135. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/builder/_stats.py +0 -0
  136. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/builder/_theta_sensitivity.py +0 -0
  137. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/builder/_writer.py +0 -0
  138. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/builder/conformance.py +0 -0
  139. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/builder/conformance_mapping.py +0 -0
  140. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/builder/dim_blocks.py +0 -0
  141. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/builder/mapping.py +0 -0
  142. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/builder/temporal_bucket.py +0 -0
  143. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/cli/__init__.py +0 -0
  144. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/cli/__main__.py +0 -0
  145. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/cli/build.py +0 -0
  146. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/cli/info.py +0 -0
  147. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/cli/main.py +0 -0
  148. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/cli/schema.py +0 -0
  149. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/cli/sources.py +0 -0
  150. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/__init__.py +0 -0
  151. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/adjacency.py +0 -0
  152. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/calibration.py +0 -0
  153. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/calibration_label_aware.py +0 -0
  154. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/chains.py +0 -0
  155. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/composition.py +0 -0
  156. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/conformance.py +0 -0
  157. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/counterfactual.py +0 -0
  158. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/density_gaps.py +0 -0
  159. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/diagnostics.py +0 -0
  160. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/dim_audit.py +0 -0
  161. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/edge_features.py +0 -0
  162. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/explanation.py +0 -0
  163. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/fdr.py +0 -0
  164. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/forecast.py +0 -0
  165. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/graph_algorithms.py +0 -0
  166. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/heredity.py +0 -0
  167. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/hop_predicate.py +0 -0
  168. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/invalidation.py +0 -0
  169. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/investigation.py +0 -0
  170. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/lance_sql_agg.py +0 -0
  171. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/p_value_calibration.py +0 -0
  172. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/selection.py +0 -0
  173. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/engine/structuring.py +0 -0
  174. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/model/__init__.py +0 -0
  175. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/model/manifest.py +0 -0
  176. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/model/objects.py +0 -0
  177. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/navigation/__init__.py +0 -0
  178. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/navigation/scanner.py +0 -0
  179. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/sphere.py +0 -0
  180. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/storage/__init__.py +0 -0
  181. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/storage/_lance_versions.py +0 -0
  182. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/storage/_schemas.py +0 -0
  183. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/storage/_tag_timestamps.py +0 -0
  184. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/storage/cache.py +0 -0
  185. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/storage/exceptions.py +0 -0
  186. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/storage/topology_cache.py +0 -0
  187. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/storage/writer.py +0 -0
  188. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/utils/__init__.py +0 -0
  189. {hypertopos-0.7.2 → hypertopos-0.7.3}/hypertopos/utils/arrow.py +0 -0
  190. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/.gitkeep +0 -0
  191. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/conftest.py +0 -0
  192. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/fixtures/generate_fixtures.py +0 -0
  193. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/fixtures/synthetic_chain_sphere.py +0 -0
  194. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_adjacency.py +0 -0
  195. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_adjacency_vectorized.py +0 -0
  196. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_aggregation_engine.py +0 -0
  197. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_anomaly_propagation_in_chain.py +0 -0
  198. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_arrow_utils.py +0 -0
  199. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_audit_label_alignment.py +0 -0
  200. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_bootstrap.py +0 -0
  201. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_bregman.py +0 -0
  202. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_builder.py +0 -0
  203. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_builder_alias.py +0 -0
  204. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_builder_chunking.py +0 -0
  205. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_builder_contagion.py +0 -0
  206. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_builder_derived.py +0 -0
  207. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_builder_edges.py +0 -0
  208. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_builder_fdr_hierarchy.py +0 -0
  209. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_builder_fdr_hierarchy_inject.py +0 -0
  210. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_builder_fdr_yaml_end_to_end.py +0 -0
  211. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_builder_rolling_z.py +0 -0
  212. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_builder_scatter.py +0 -0
  213. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_builder_temporal_bucket.py +0 -0
  214. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_builder_temporal_perf.py +0 -0
  215. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_cache.py +0 -0
  216. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_calibration.py +0 -0
  217. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_calibration_drift.py +0 -0
  218. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_calibration_history.py +0 -0
  219. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_calibration_history_builder.py +0 -0
  220. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_calibration_history_reader.py +0 -0
  221. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_calibration_label_aware.py +0 -0
  222. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_chain_coherent_anomaly.py +0 -0
  223. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_chain_coherent_synthetic.py +0 -0
  224. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_chain_drift_trajectory.py +0 -0
  225. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_chain_edge_potentials.py +0 -0
  226. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_chain_investigation_summary.py +0 -0
  227. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_chain_typology_and_extend.py +0 -0
  228. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_chain_witness_intersection.py +0 -0
  229. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_chains.py +0 -0
  230. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_cli.py +0 -0
  231. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_cli_alias.py +0 -0
  232. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_cli_label_aware_flag.py +0 -0
  233. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_cli_sources.py +0 -0
  234. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_compaction.py +0 -0
  235. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_composition.py +0 -0
  236. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_conformance.py +0 -0
  237. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_conformance_mapping.py +0 -0
  238. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_conftest_magicmock_restore.py +0 -0
  239. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_continuous_event_dims.py +0 -0
  240. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_counterfactual.py +0 -0
  241. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_counterfactual_frozen_trajectory.py +0 -0
  242. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_data_quality_prop.py +0 -0
  243. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_decompose_drift.py +0 -0
  244. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_delta_dim_aggregation_consumers.py +0 -0
  245. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_delta_norm_signed.py +0 -0
  246. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_density_gaps_engine.py +0 -0
  247. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_detection.py +0 -0
  248. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_diagnostics.py +0 -0
  249. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_dim_audit.py +0 -0
  250. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_dim_blocks.py +0 -0
  251. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_dim_quality_warnings.py +0 -0
  252. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_distinct_neighbor_count.py +0 -0
  253. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_edge_dim_aggregations.py +0 -0
  254. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_edge_dim_aggregations_builder.py +0 -0
  255. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_edge_dim_aggregations_chain.py +0 -0
  256. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_edge_dim_aggregations_engine.py +0 -0
  257. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_edge_dim_aggregations_roundtrip.py +0 -0
  258. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_edge_dimensions_builder.py +0 -0
  259. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_edge_dimensions_yaml.py +0 -0
  260. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_edge_features.py +0 -0
  261. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_edge_features_reader.py +0 -0
  262. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_edge_potential.py +0 -0
  263. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_edge_table.py +0 -0
  264. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_engine.py +0 -0
  265. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_fdr.py +0 -0
  266. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_fdr_multi_resolution.py +0 -0
  267. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_fdr_per_dim.py +0 -0
  268. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_find_calibration_influencers.py +0 -0
  269. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_find_density_gaps.py +0 -0
  270. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_find_diverse_explanations.py +0 -0
  271. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_find_lead_lag.py +0 -0
  272. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_find_lead_lag_significance.py +0 -0
  273. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_find_motif_by_hops.py +0 -0
  274. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_forecast.py +0 -0
  275. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_fts.py +0 -0
  276. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_generate_sar_rationale.py +0 -0
  277. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_geometric_path.py +0 -0
  278. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_geometry_stats.py +0 -0
  279. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_gradient_alignment.py +0 -0
  280. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_graph_algorithms.py +0 -0
  281. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_graph_geometry_tension.py +0 -0
  282. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_heredity.py +0 -0
  283. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_hop_predicate_engine.py +0 -0
  284. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_hub_safety_caps.py +0 -0
  285. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_hybrid_search.py +0 -0
  286. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_incremental.py +0 -0
  287. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_invalidation.py +0 -0
  288. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_investigate_chain.py +0 -0
  289. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_investigate_entity.py +0 -0
  290. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_investigation.py +0 -0
  291. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_kind_detect.py +0 -0
  292. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_kind_tag_mismatch_warning.py +0 -0
  293. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_label_audit_schema.py +0 -0
  294. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_lance_sql_agg.py +0 -0
  295. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_m1_1_builder_wiring.py +0 -0
  296. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_mapping.py +0 -0
  297. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_model.py +0 -0
  298. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_motif.py +0 -0
  299. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_mvcc_session.py +0 -0
  300. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_native_mvcc_geometry.py +0 -0
  301. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_navigator_alerts.py +0 -0
  302. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_navigator_fdr_resolution.py +0 -0
  303. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_navigator_fdr_select.py +0 -0
  304. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_neighbors_window.py +0 -0
  305. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_observability.py +0 -0
  306. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_p_value_calibration.py +0 -0
  307. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_pattern_fdr_hierarchy.py +0 -0
  308. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_precision_stack.py +0 -0
  309. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_profile_motif_perf_smoke.py +0 -0
  310. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_reliability_flags.py +0 -0
  311. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_reputation.py +0 -0
  312. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_root_cause_trace.py +0 -0
  313. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_row_id_cache.py +0 -0
  314. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_scanner.py +0 -0
  315. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_schema_chain_eda_validation.py +0 -0
  316. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_selection.py +0 -0
  317. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_signed_confidence_ranking.py +0 -0
  318. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_simulate_dimension_change.py +0 -0
  319. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_sphere.py +0 -0
  320. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_sphere_format_version.py +0 -0
  321. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_sphere_overview_perf.py +0 -0
  322. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_sphere_validation_cheap_tier.py +0 -0
  323. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_stats_guard.py +0 -0
  324. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_storage.py +0 -0
  325. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_streaming_reads.py +0 -0
  326. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_structuring_engine.py +0 -0
  327. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_tag_timestamps.py +0 -0
  328. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_temporal_centroids.py +0 -0
  329. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_theta_sensitivity.py +0 -0
  330. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_topology.py +0 -0
  331. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_topology_anomalies.py +0 -0
  332. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_topology_cache.py +0 -0
  333. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_topology_trajectory_anomalies.py +0 -0
  334. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_trajectory_ann.py +0 -0
  335. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_witness_cohort.py +0 -0
  336. {hypertopos-0.7.2 → hypertopos-0.7.3}/tests/test_witness_set.py +0 -0
@@ -7,6 +7,28 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.3] — 2026-05-27
11
+
12
+ ### Added
13
+
14
+ - `GDSNavigator.π12_attract_regime_change` (and its cache fast-path) — each detected changepoint now carries a `near_data_boundary: bool` field. Fires `True` when the changepoint timestamp index falls in the first two or last two temporal buckets of the filtered range — agents can suppress these as low-confidence detections sitting on insufficient one-sided context (the buckets at the data range edges have asymmetric neighbours, which inflates the shift magnitude). Applies to both the cache fast-path (`_pi12_from_cache`) and the cold-scan path; no schema change.
15
+ - `Pattern.signed_percentiles` — per-pattern percentile cache (`p1`, `p5`, `p50`, `p95`, `p99`) over the `delta_norm_signed` Lance column, populated at build time when label-aware calibration fires. Drives the new `signed_tail_concentration` dim_quality warning that flags one-sided extreme tails (`|p99| / max(|p50|, 1e-9) > 50`) on the Fisher LDA-projected delta distribution; suppressed when `Pattern.label_aware_n_pos < 30` to avoid noise from undersampled positive classes.
16
+ - `GDSEngine.decompose_displacement(delta, label_direction)` — pure-math helper that splits a polygon delta vector along (`intrinsic`) and orthogonal to (`extrinsic`) any label-discriminating axis, satisfying `intrinsic^2 + extrinsic^2 == ||delta||^2`. Pattern-level means `intrinsic_displacement_mean` / `extrinsic_displacement_mean` are persisted on `Pattern` at build time when label-aware calibration fires.
17
+ - `Pattern.label_aware_n_pos` / `Pattern.label_aware_n_neg` — positive / negative labelled sample counts captured at build time, co-populated with `label_aware_calibration`.
18
+ - `GDSNavigator.chain_signed_confidence_rollup(chain_id, chain_pattern=..., anchor_pattern=...)` — chain-level reliability rollup derived from per-member `signed_confidence` ranking on the anchor pattern. Returns `chain_mean_signed_confidence`, `chain_n_low_confidence_members`, `chain_n_single_dim_driven_members`, and `chain_confidence_verdict` ∈ {high, medium, low, label-aware-unavailable}. Falls back cleanly with verdict `"label-aware-unavailable"` and null numeric fields when the anchor pattern lacks `label_aware_calibration`.
19
+ - `GDSNavigator.aggregate` per-entity result rows gain `anomaly_rate: float | null` — fraction of an entity's events flagged anomalous (`delta_norm >= theta_diag`). `null` when entity has zero events or pattern lacks anomaly threshold.
20
+ - `GDSNavigator.sphere_overview` gains `cross_pattern_discrepancy: dict | null` — pairwise Jaccard overlap of anomalous primary_keys across cover-overlapping patterns. Lists pairs with `pattern_a`, `pattern_b`, `shared_line`, the four anomaly-bucket counts, and `jaccard_anomaly_overlap`. `null` on spheres with fewer than two cover-overlapping patterns.
21
+ - `GDSNavigator.find_anomalies(..., sample_size: int | None = None, boundary_aware: bool = False)` — opt-in sampling for the in-process scan path. `sample_size` caps geometry rows scored; `boundary_aware=True` allocates half the budget to entities within the `[0.8 * theta_norm, 1.2 * theta_norm]` band around the decision threshold, the other half drawn from the rest of the population. Defaults preserve the existing full-scan behaviour. `boundary_aware=True` requires `sample_size` to be set.
22
+ - `engine.topology.classify_trajectory(solid_table, *, sample_size=10000)` — wraps `trajectory_continuous_score` with a deterministic 4-category classifier (`outlier` / `lagging` / `leading` / `typical`). Combines DTW distance against the population-median trajectory with a first-derivative slope comparison via `scipy.stats.linregress`. Returns a list of `{primary_key, dtw_distance, category, category_evidence}` per entity, with `category_evidence` carrying the signed deviation that drove the classification.
23
+ - `GDSNavigator.classify_trajectory(primary_key, pattern_id, sample_size=10000)` — per-entity navigator wrapper that streams temporal `shape_snapshot` rows, projects to `delta_snapshot` via the pattern's `(mu, sigma_diag)`, and returns the classification for the requested entity.
24
+ - `sphere.yaml` `graph_features.features:` accepts a new token — `community_id` — Louvain modularity-optimizing community detection on the anchor-pattern graph. `igraph.Graph.community_multilevel` when igraph is installed; `networkx.algorithms.community.louvain_communities` otherwise; gracefully emits an empty mapping (caller fills nulls) when neither backend is available. Community IDs are renumbered so the largest community is `0`.
25
+ - `graph_features.features:` `pagerank` and `connected_component` now route through pure-numpy helpers (damped PageRank with damping `0.85`, max `100` iterations, L2 convergence threshold `1e-6`; Union-Find connected components with path compression and union-by-rank). No external graph library is required for these two; igraph remains optional and is used only for the other features in the block.
26
+ - `engine.graph.compute_pagerank`, `compute_louvain_community`, `compute_connected_components`, `compute_from_adjacency` — pure helpers exposed for direct use on an `AdjacencyIndex`. PageRank parameters (`damping`, `max_iter`, `tol`) are configurable per call.
27
+ - `GDSNavigator.find_calibration_influencers(..., auto_discover: bool = False, auto_k: int = 10)` — auto-discovery mode runs k-means on the pattern's geometry, surfaces top-K candidate influencers from cluster centroids, then ranks each by existing μ-impact algorithm. Returns the existing shape extended with per-candidate `cluster_size` and `cluster_centroid_distance`. Default `auto_discover=False` preserves manual mode requiring caller-supplied candidate_keys.
28
+ - `GDSNavigator.calibration_influencer_history(primary_key, *, pattern_id)` — per-epoch μ-impact history for a known influencer. Lazily populated write-through cache at `_gds_meta/calibration_history/<pattern>/influencer_<primary_key>.json`. Returns chronological list of `{epoch, calibrated_at, mu_impact, delta_norm_impact}` entries. Empty list + warning when cache absent.
29
+ - `engine.topology.local_trajectory_shape(delta_norms)` — pure-function classifier that categorises an entity's own `delta_norm` time-series as `arch` / `V` / `linear` / `flat` without requiring a population reference. Returns `None` when fewer than three samples are available. Used by `dive_solid` to surface trajectory shape on the entity's temporal slices.
30
+ - `Navigator.find_similar_entities(..., with_neighbor_anomaly=False)` — opt-in flag triggers a single point lookup over the returned top-N neighbour keys and populates the `SimilarityResult.is_anomaly_map` sidecar with stored anomaly flags. Existing 2-tuple iteration is preserved.
31
+
10
32
  ## [0.7.2] — 2026-05-21
11
33
 
12
34
  ### Added
@@ -26,7 +26,7 @@ For purposes of this License:
26
26
  - **Internal use** within an organization is not a competitive offering. Your
27
27
  organization includes entities under common control.
28
28
 
29
- **Change Date:** 2029-05-21
29
+ **Change Date:** 2029-05-27
30
30
 
31
31
  **Change License:** Apache License, Version 2.0
32
32
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypertopos
3
- Version: 0.7.2
3
+ Version: 0.7.3
4
4
  Summary: Understand the structure of your data — without training machine learning models
5
5
  Project-URL: Homepage, https://github.com/hypertopos/hypertopos-py
6
6
  Project-URL: Documentation, https://github.com/hypertopos/hypertopos-py/tree/main/docs
@@ -9,7 +9,7 @@
9
9
  [![PyArrow](https://img.shields.io/badge/format-PyArrow-red.svg)](https://arrow.apache.org/docs/python/)
10
10
  [![Lance](https://img.shields.io/badge/storage-Lance-blueviolet.svg)](https://github.com/lance-format/lance)
11
11
  [![MCP](https://img.shields.io/badge/MCP-compatible-green.svg)](https://modelcontextprotocol.io)
12
- [![Version](https://img.shields.io/badge/version-0.7.2-%235500FF.svg)](pyproject.toml)
12
+ [![Version](https://img.shields.io/badge/version-0.7.3-%235500FF.svg)](pyproject.toml)
13
13
 
14
14
  hypertopos is not a database, and not a machine learning model. It is a layer that turns relational data into a coordinate system where every entity gets a position derived from its relationships and the population around it.
15
15
 
@@ -7,6 +7,9 @@
7
7
  Detector composition and counterfactual primitives on native Lance MVCC — multi-detector consensus, per-edge / joint / per-counterparty influence math, topological cycle persistence, multi-resolution FDR, and the entity-side investigation orchestrator land on a modernized Lance 6.0 storage substrate with sphere format 3.0.
8
8
 
9
9
  - **0.7.0** — Lance 4.0 → 6.0 bump; sphere format 2.4 → 3.0 (breaking — single Lance dataset per pattern, calibration epochs tagged `epoch_<N>`, no migration tool); Wilson HMP combiner + `combine_anomaly_pvalues` + `classify_detector_consensus` over five orthogonal anomaly axes; counterfactual family (`simulate_edge_removal`, `select_minimal_joint_edge_removal`, `simulate_counterparty_removal`); `find_topological_anomalies` H_1 cycle persistence + `find_topological_trajectory_anomalies`; `find_graph_geometry_tension` 2×2 behavioural-vs-graph cross-tab; multi-resolution FDR with `Pattern.fdr_hierarchy` + `fdr_temporal_hierarchy`; per-dim FDR axis + `rank_by="min_q_per_dim"`; `investigate_entity` orchestrator; `edge_curvature_frc` Forman-Ricci edge dimension; `reliability_flags` triage on five primitive outputs; `composite_risk` switches Fisher → HMP.
10
+ - **0.7.1** — label-aware calibration foundation: `label_audit:` YAML block, per-class μ/σ + Cohen's d + Fisher LDA direction on `Pattern`, `delta_norm_signed` Lance column, `audit_pattern_dims` full per-class field path, Levene's test homoscedasticity diagnostic in `dim_quality_warnings`.
11
+ - **0.7.2** — composition orchestrators on the 0.7.1 foundation: `find_anomalies(rank_by="signed_confidence")`, `chain_full_loop_summary` chain-side mirror of `investigate_entity`, `audit_label_alignment` Fisher LDA AUROC sibling to `audit_pattern_dims`, `kind_mismatch` warning + `recommended_action="kind_mismatch_review"`, `dive_solid(counterfactual_frozen_population=True)`, `Chain.to_dict() edge_potentials`, `changelog.d/` per-PR fragments convention.
12
+ - **0.7.3** — detector-composition continuation + investigation-orchestrator surface: per-dim AUROC + intrinsic / extrinsic decomposition on `audit_pattern_dims`, `signed_tail_concentration` warning, chain reliability rollup on `chain_full_loop_summary`, `find_anomalies(sample_size, boundary_aware)`, `classify_trajectory` + `local_trajectory_shape`, `sphere_overview.cross_pattern_discrepancy` + per-row `anomaly_rate`, `community_id` graph feature + pure-NumPy `pagerank` / `connected_component`, `find_calibration_influencers(auto_discover=True)` + `calibration_influencer_history`, `π12_attract_regime_change.near_data_boundary` flag, `find_similar_entities(with_neighbor_anomaly=True)`, `passive_scan` interpretation hint, `dive_solid.trajectory_shape`.
10
13
 
11
14
  See [CHANGELOG](CHANGELOG.md) for the full feature list.
12
15
 
@@ -1,6 +1,6 @@
1
1
  # Security
2
2
 
3
- ## Current version: 0.7.2
3
+ ## Current version: 0.7.3
4
4
 
5
5
  hypertopos runs locally. No network services, no auth layer, no multi-tenancy.
6
6
 
@@ -44,4 +44,4 @@ __all__ = [
44
44
  "IntrinsicExtrinsicReport",
45
45
  "LeadLagReport",
46
46
  ]
47
- __version__ = "0.7.2"
47
+ __version__ = "0.7.3"
@@ -624,6 +624,15 @@ class PatternBuildResult:
624
624
  # into sphere.json as ``label_aware_calibration`` and hydrated back
625
625
  # into ``Pattern.label_aware_calibration`` by the storage reader.
626
626
  label_aware_calibration: dict[str, Any] | None = None
627
+ # Co-populated with ``label_aware_calibration`` — positive / negative
628
+ # labelled sample counts and the percentiles + intrinsic / extrinsic
629
+ # displacement means computed from the signed delta projection. ``None``
630
+ # on patterns without label-aware calibration.
631
+ label_aware_n_pos: int | None = None
632
+ label_aware_n_neg: int | None = None
633
+ signed_percentiles: dict[str, float] | None = None
634
+ intrinsic_displacement_mean: float | None = None
635
+ extrinsic_displacement_mean: float | None = None
627
636
 
628
637
 
629
638
  @dataclass
@@ -822,7 +831,8 @@ class GDSBuilder:
822
831
  ) -> dict[str, Any] | None:
823
832
  """Fit label-aware per-dim calibration when the pattern opts in.
824
833
 
825
- Returns the per-dim mapping ``{dim_label: DimCalibration}`` and
834
+ Returns a bundle ``{"per_dim": {dim_label: DimCalibration},
835
+ "n_pos": int, "n_neg": int, "direction": np.ndarray}`` and
826
836
  registers the global Fisher LDA direction in
827
837
  ``self._label_aware_directions[pat.pattern_id]`` so the downstream
828
838
  geometry pass can populate ``delta_norm_signed``. Returns ``None``
@@ -924,10 +934,16 @@ class GDSBuilder:
924
934
  )
925
935
  return None
926
936
 
927
- self._label_aware_directions[pat.pattern_id] = np.asarray(
937
+ direction_vec = np.asarray(
928
938
  result.signed_direction_vector, dtype=np.float32,
929
939
  )
930
- return dict(result.per_dim)
940
+ self._label_aware_directions[pat.pattern_id] = direction_vec
941
+ return {
942
+ "per_dim": dict(result.per_dim),
943
+ "n_pos": int(result.n_pos),
944
+ "n_neg": int(result.n_neg),
945
+ "direction": direction_vec,
946
+ }
931
947
 
932
948
  def add_line(
933
949
  self,
@@ -2916,6 +2932,105 @@ class GDSBuilder:
2916
2932
 
2917
2933
  return result
2918
2934
 
2935
+ @staticmethod
2936
+ def _unpack_lac_bundle(
2937
+ lac_bundle: dict[str, Any] | None,
2938
+ deltas: np.ndarray | None,
2939
+ ) -> dict[str, Any]:
2940
+ """Translate the ``_run_label_aware_calibration`` bundle into
2941
+ PBR kwargs.
2942
+
2943
+ Returns a dict with keys ``label_aware_calibration``,
2944
+ ``label_aware_n_pos``, ``label_aware_n_neg``,
2945
+ ``signed_percentiles``, ``intrinsic_displacement_mean``,
2946
+ ``extrinsic_displacement_mean`` — every value ``None`` when the
2947
+ bundle is missing or the delta matrix is unavailable, ready to
2948
+ splat into PatternBuildResult.
2949
+ """
2950
+ if lac_bundle is None or deltas is None or deltas.ndim != 2:
2951
+ return {
2952
+ "label_aware_calibration": None,
2953
+ "label_aware_n_pos": None,
2954
+ "label_aware_n_neg": None,
2955
+ "signed_percentiles": None,
2956
+ "intrinsic_displacement_mean": None,
2957
+ "extrinsic_displacement_mean": None,
2958
+ }
2959
+ direction = lac_bundle.get("direction")
2960
+ if direction is None or direction.shape[0] != deltas.shape[1]:
2961
+ return {
2962
+ "label_aware_calibration": lac_bundle.get("per_dim"),
2963
+ "label_aware_n_pos": lac_bundle.get("n_pos"),
2964
+ "label_aware_n_neg": lac_bundle.get("n_neg"),
2965
+ "signed_percentiles": None,
2966
+ "intrinsic_displacement_mean": None,
2967
+ "extrinsic_displacement_mean": None,
2968
+ }
2969
+ signed_p, intr_mean, extr_mean = (
2970
+ GDSBuilder._compute_signed_artefacts(deltas, direction)
2971
+ )
2972
+ return {
2973
+ "label_aware_calibration": lac_bundle.get("per_dim"),
2974
+ "label_aware_n_pos": lac_bundle.get("n_pos"),
2975
+ "label_aware_n_neg": lac_bundle.get("n_neg"),
2976
+ "signed_percentiles": signed_p or None,
2977
+ "intrinsic_displacement_mean": intr_mean,
2978
+ "extrinsic_displacement_mean": extr_mean,
2979
+ }
2980
+
2981
+ @staticmethod
2982
+ def _compute_signed_artefacts(
2983
+ deltas: np.ndarray,
2984
+ direction: np.ndarray,
2985
+ ) -> tuple[dict[str, float], float, float]:
2986
+ """Derive signed-projection artefacts from deltas + Fisher direction.
2987
+
2988
+ Computes:
2989
+
2990
+ - ``signed_percentiles``: dict with keys ``p1``, ``p5``, ``p50``,
2991
+ ``p95``, ``p99`` of the signed projection
2992
+ ``deltas @ direction_unit``. The LDA fit returns a unit-norm
2993
+ direction so today's caller passes the same vector the
2994
+ geometry pass uses to populate ``delta_norm_signed``; we still
2995
+ normalise defensively so any future non-unit caller gets the
2996
+ contract everyone else assumes.
2997
+ - ``intrinsic_displacement_mean``: mean of
2998
+ ``|delta . direction_unit|`` across rows. Magnitude of motion
2999
+ along the label axis.
3000
+ - ``extrinsic_displacement_mean``: mean of
3001
+ ``sqrt(||delta||^2 - intrinsic^2)`` across rows. Magnitude of
3002
+ motion orthogonal to the label axis.
3003
+
3004
+ Returns empty percentiles dict + (0.0, 0.0) when the delta matrix
3005
+ is empty or the direction is the zero vector.
3006
+ """
3007
+ if deltas.ndim != 2 or deltas.shape[0] == 0:
3008
+ return {}, 0.0, 0.0
3009
+ d = deltas.astype(np.float64, copy=False)
3010
+ v = direction.astype(np.float64, copy=False)
3011
+ v_norm = float(np.linalg.norm(v))
3012
+ if v_norm <= 0.0:
3013
+ return {}, 0.0, 0.0
3014
+ v = v / v_norm
3015
+ projected = d @ v
3016
+ percs = np.percentile(projected, [1, 5, 50, 95, 99])
3017
+ signed_percentiles = {
3018
+ "p1": round(float(percs[0]), 4),
3019
+ "p5": round(float(percs[1]), 4),
3020
+ "p50": round(float(percs[2]), 4),
3021
+ "p95": round(float(percs[3]), 4),
3022
+ "p99": round(float(percs[4]), 4),
3023
+ }
3024
+ intrinsic = np.abs(projected)
3025
+ row_norms_sq = np.einsum("ij,ij->i", d, d)
3026
+ residual_sq = np.maximum(row_norms_sq - intrinsic * intrinsic, 0.0)
3027
+ extrinsic = np.sqrt(residual_sq)
3028
+ return (
3029
+ signed_percentiles,
3030
+ float(intrinsic.mean()),
3031
+ float(extrinsic.mean()),
3032
+ )
3033
+
2919
3034
  def _compute_dim_percentiles(
2920
3035
  self,
2921
3036
  entity_line_id: str,
@@ -3221,6 +3336,20 @@ class GDSBuilder:
3221
3336
  }
3222
3337
  for label, dc in pbr.label_aware_calibration.items()
3223
3338
  }
3339
+ if pbr.label_aware_n_pos is not None:
3340
+ pat_dict["label_aware_n_pos"] = int(pbr.label_aware_n_pos)
3341
+ if pbr.label_aware_n_neg is not None:
3342
+ pat_dict["label_aware_n_neg"] = int(pbr.label_aware_n_neg)
3343
+ if pbr.signed_percentiles:
3344
+ pat_dict["signed_percentiles"] = pbr.signed_percentiles
3345
+ if pbr.intrinsic_displacement_mean is not None:
3346
+ pat_dict["intrinsic_displacement_mean"] = float(
3347
+ pbr.intrinsic_displacement_mean,
3348
+ )
3349
+ if pbr.extrinsic_displacement_mean is not None:
3350
+ pat_dict["extrinsic_displacement_mean"] = float(
3351
+ pbr.extrinsic_displacement_mean,
3352
+ )
3224
3353
  if pat.description:
3225
3354
  pat_dict["description"] = pat.description
3226
3355
  if pat.fdr_hierarchy:
@@ -3767,7 +3896,7 @@ class GDSBuilder:
3767
3896
  edge_dim_agg_labels=ps.edge_dim_agg_labels,
3768
3897
  ),
3769
3898
  edge_dim_names=ps.edge_dim_names,
3770
- label_aware_calibration=lac,
3899
+ **self._unpack_lac_bundle(lac, ps.deltas),
3771
3900
  )
3772
3901
  self._write_calibration_epoch_for_pattern(pat, pbr)
3773
3902
  return pat_id, pbr
@@ -4671,7 +4800,7 @@ class GDSBuilder:
4671
4800
  edge_dim_agg_labels=ps.edge_dim_agg_labels,
4672
4801
  ),
4673
4802
  edge_dim_names=ps.edge_dim_names,
4674
- label_aware_calibration=lac,
4803
+ **self._unpack_lac_bundle(lac, ps.deltas),
4675
4804
  )
4676
4805
 
4677
4806
  def _build_shape_chunk(
@@ -833,15 +833,39 @@ def compute_graph_features(
833
833
  # --- Graph algorithm features (computed on AdjacencyIndex) ---
834
834
  _ALGO_FEATURES = {
835
835
  "pagerank", "connected_component", "clustering_coefficient",
836
- "community", "betweenness",
836
+ "community", "community_id", "betweenness",
837
837
  }
838
838
  needed_algos = _ALGO_FEATURES & set(features)
839
839
  if needed_algos:
840
- from hypertopos.engine.graph_algorithms import compute_all_from_lists
841
-
842
840
  f_list = edges["_f"].to_pylist()
843
841
  t_list = edges["_t"].to_pylist()
844
- algo_results = compute_all_from_lists(f_list, t_list, needed_algos)
842
+
843
+ # pagerank / community_id / connected_component go through engine.graph
844
+ # (pure-numpy / igraph→NetworkX fallback chain). Remaining algos
845
+ # (clustering_coefficient, betweenness, label-prop "community") stay
846
+ # on the igraph fast path in graph_algorithms.
847
+ algo_results: dict[str, dict] = {}
848
+ _GRAPH_HELPERS = {"pagerank", "community_id", "connected_component"}
849
+ helper_algos = _GRAPH_HELPERS & needed_algos
850
+ if helper_algos:
851
+ from hypertopos.engine.adjacency import AdjacencyIndex
852
+ from hypertopos.engine.graph import compute_from_adjacency
853
+ n_edges_lance = len(f_list)
854
+ adj_for_helpers = AdjacencyIndex.from_edge_lists(
855
+ f_list, t_list,
856
+ [0.0] * n_edges_lance, [0.0] * n_edges_lance,
857
+ [f"_e{i}" for i in range(n_edges_lance)],
858
+ )
859
+ algo_results.update(
860
+ compute_from_adjacency(adj_for_helpers, helper_algos),
861
+ )
862
+
863
+ remaining_algos = needed_algos - _GRAPH_HELPERS
864
+ if remaining_algos:
865
+ from hypertopos.engine.graph_algorithms import compute_all_from_lists
866
+ algo_results.update(
867
+ compute_all_from_lists(f_list, t_list, remaining_algos),
868
+ )
845
869
 
846
870
  _EDGE_MAX_OVERRIDES = {
847
871
  "clustering_coefficient": 1,
@@ -853,7 +877,7 @@ def compute_graph_features(
853
877
  idx = key_to_idx.get(k)
854
878
  if idx is not None:
855
879
  vals[idx] = float(v)
856
- if feat_name in ("connected_component", "community"):
880
+ if feat_name in ("connected_component", "community", "community_id"):
857
881
  n_unique = len(set(scores.values())) if scores else 1
858
882
  edge_max = max(n_unique - 1, 1)
859
883
  elif feat_name in _EDGE_MAX_OVERRIDES:
@@ -898,7 +922,7 @@ def compute_graph_features_temporal(
898
922
  # PageRank/betweenness is meaningless and extremely expensive)
899
923
  STATIC_ONLY_FEATURES = {
900
924
  "pagerank", "connected_component", "clustering_coefficient",
901
- "community", "betweenness",
925
+ "community", "community_id", "betweenness",
902
926
  }
903
927
  fast_indices = [i for i, f in enumerate(features) if f in FAST_FEATURES]
904
928
  need_recip = "reciprocity" in features
@@ -638,6 +638,18 @@ def aggregate(
638
638
  f"with a '{group_by_line}' edge. "
639
639
  f"Use anomaly_summary for population-wide totals."
640
640
  )
641
+ # Per-group anomaly_rate enrichment also runs on the reversed-scan
642
+ # fast path so the field is uniformly present regardless of which
643
+ # execution branch produced the counts.
644
+ if _should_emit_per_group_anomaly_rate(
645
+ False, metric, group_by_property, pivot_event_field,
646
+ group_by_line_2, distinct, result,
647
+ ):
648
+ _enrich_per_group_anomaly_rate(
649
+ reader, event_pattern_id, pattern, group_by_line,
650
+ edge_col="entity_keys",
651
+ result=result,
652
+ )
641
653
  return result
642
654
  except Exception:
643
655
  pass # Fall through to read_geometry path
@@ -2185,4 +2197,105 @@ def aggregate(
2185
2197
  row["total_events"] = total
2186
2198
  row["anomaly_rate"] = round(row["count"] / total, 4) if total > 0 else 0.0
2187
2199
 
2200
+ # --- Per-group anomaly_rate enrichment (inverse of _anomaly_rate_requested) ---
2201
+ # When metric='count' without an is_anomaly filter, each row['count'] is the
2202
+ # total event count for the group. Surface the share of anomalous events by
2203
+ # doing a second read filtered to is_anomaly=True and dividing.
2204
+ # Skipped when the existing block already wrote anomaly_rate (above), when
2205
+ # the metric is not 'count' (rate is undefined for sum/avg/median), when
2206
+ # composite keys are used (group_by_property / pivot_event_field /
2207
+ # group_by_line_2 — the row key is not a single group_by_line edge target),
2208
+ # and when no rows were returned.
2209
+ if _should_emit_per_group_anomaly_rate(
2210
+ _anomaly_rate_requested, metric, group_by_property, pivot_event_field,
2211
+ group_by_line_2, distinct, result,
2212
+ ):
2213
+ _enrich_per_group_anomaly_rate(
2214
+ reader, event_pattern_id, pattern, group_by_line,
2215
+ edge_col=("entity_keys" if "entity_keys" in geo.schema.names else "edges"),
2216
+ result=result,
2217
+ )
2218
+
2188
2219
  return result
2220
+
2221
+
2222
+ def _should_emit_per_group_anomaly_rate(
2223
+ anomaly_rate_requested: bool,
2224
+ metric: str,
2225
+ group_by_property: str | None,
2226
+ pivot_event_field: str | None,
2227
+ group_by_line_2: str | None,
2228
+ distinct: bool,
2229
+ result: dict,
2230
+ ) -> bool:
2231
+ """Predicate for the inverse anomaly_rate enrichment.
2232
+
2233
+ Triggers when the row's ``count`` is the total event count for the group
2234
+ (per-row composite keys excluded) and the existing "filter-by-is_anomaly"
2235
+ block has not already attached ``anomaly_rate`` to each row.
2236
+ """
2237
+ return (
2238
+ not anomaly_rate_requested
2239
+ and metric == "count"
2240
+ and not group_by_property
2241
+ and not pivot_event_field
2242
+ and not group_by_line_2
2243
+ and not distinct
2244
+ and bool(result.get("results"))
2245
+ )
2246
+
2247
+
2248
+ def _enrich_per_group_anomaly_rate(
2249
+ reader: Any,
2250
+ event_pattern_id: str,
2251
+ pattern: Any,
2252
+ group_by_line: str,
2253
+ *,
2254
+ edge_col: str,
2255
+ result: dict,
2256
+ ) -> None:
2257
+ """Attach ``anomaly_rate = n_anomalous_in_group / total_in_group`` per row.
2258
+
2259
+ Performs one extra geometry read scoped to two columns (``is_anomaly`` +
2260
+ edge column) with a Lance ``is_anomaly = true`` filter pushed down via
2261
+ the bitmap index so only anomalous rows materialise. Counts anomalous
2262
+ edges per group and divides into the row's existing ``count``.
2263
+ ``anomaly_rate`` is ``None`` when the group's total count is zero.
2264
+ The field is omitted when the geometry on disk has no ``is_anomaly``
2265
+ column (degraded / pre-schema spheres) — no zero-div crash, no
2266
+ spurious value.
2267
+ """
2268
+ try:
2269
+ raw = reader.read_geometry(
2270
+ event_pattern_id, pattern.version,
2271
+ columns=["is_anomaly", edge_col],
2272
+ filter="is_anomaly = true",
2273
+ )
2274
+ except Exception:
2275
+ # Reader rejected the filter syntax (mock / pre-Lance reader) — fall
2276
+ # back to a no-filter read; the in-process predicate below still
2277
+ # narrows to anomalous rows.
2278
+ raw = reader.read_geometry(
2279
+ event_pattern_id, pattern.version,
2280
+ columns=["is_anomaly", edge_col],
2281
+ )
2282
+ if "is_anomaly" not in raw.schema.names:
2283
+ # Column was projected out (degraded / pre-schema sphere) —
2284
+ # cannot compute the rate. Leave rows unannotated.
2285
+ return
2286
+ import pyarrow.compute as pc
2287
+ # Apply the predicate in-process too — Lance may or may not honour the
2288
+ # pushed-down filter (mock readers silently ignore it). This guarantees
2289
+ # ``anom_only`` is strictly the anomalous subset regardless.
2290
+ anom_only = raw.filter(pc.equal(raw["is_anomaly"], True))
2291
+ anom_per_group, _ = _vectorized_count_with_warning(
2292
+ anom_only, group_by_line, geometry_filters=None, relations=pattern.relations,
2293
+ )
2294
+ for row in result.get("results", []):
2295
+ key = row.get("key")
2296
+ total = row.get("count")
2297
+ if key is None or total is None or total == 0:
2298
+ row["anomaly_rate"] = None
2299
+ continue
2300
+ n_anom = anom_per_group.get(key, 0)
2301
+ row["anomaly_rate"] = round(n_anom / total, 4)
@@ -178,6 +178,62 @@ class GDSEngine:
178
178
  delta = delta * pattern.dimension_weights
179
179
  return delta
180
180
 
181
+ @staticmethod
182
+ def decompose_displacement(
183
+ delta: np.ndarray,
184
+ label_direction: np.ndarray,
185
+ ) -> dict[str, float]:
186
+ """Decompose a single delta vector along the label-discriminating axis.
187
+
188
+ Splits ``delta`` into two scalar magnitudes:
189
+
190
+ - ``intrinsic`` = ``|delta . label_direction_unit|`` — magnitude of
191
+ the projection onto the unit-norm label direction. Captures how
192
+ far the entity moved along the axis that separates labelled
193
+ classes.
194
+ - ``extrinsic`` = ``sqrt(||delta||^2 - intrinsic^2)`` — magnitude of
195
+ the residual component orthogonal to the label axis.
196
+
197
+ Identity: ``intrinsic^2 + extrinsic^2 == ||delta||^2`` to floating
198
+ precision. ``label_direction`` is normalised internally — callers
199
+ may pass any non-zero vector.
200
+
201
+ This is the label-axis decomposition of a single polygon's delta
202
+ vector. It is NOT the calibration-drift intrinsic/extrinsic
203
+ decomposition (see ``_compute_intrinsic_extrinsic_decomposition``)
204
+ which splits drift between two calibration epochs into a
205
+ within-entity-shape component vs a coordinate-system component —
206
+ same vocabulary, different math.
207
+
208
+ Args:
209
+ delta: 1-D delta vector for one polygon.
210
+ label_direction: 1-D vector of the same length, must be
211
+ non-zero. Direction sense is irrelevant (``intrinsic``
212
+ is absolute-valued).
213
+
214
+ Returns:
215
+ ``{"intrinsic": float, "extrinsic": float}``. Both
216
+ non-negative. When ``label_direction`` is the zero vector
217
+ both values are 0.0 (no axis to project onto, no decomposition
218
+ defined).
219
+ """
220
+ d = np.asarray(delta, dtype=np.float64).ravel()
221
+ ld = np.asarray(label_direction, dtype=np.float64).ravel()
222
+ if d.shape != ld.shape:
223
+ raise ValueError(
224
+ f"delta and label_direction shape mismatch: "
225
+ f"{d.shape} vs {ld.shape}",
226
+ )
227
+ ld_norm = float(np.linalg.norm(ld))
228
+ if ld_norm <= 0.0:
229
+ return {"intrinsic": 0.0, "extrinsic": 0.0}
230
+ ld_unit = ld / ld_norm
231
+ intrinsic = float(abs(np.dot(d, ld_unit)))
232
+ d_sq = float(np.dot(d, d))
233
+ residual_sq = max(d_sq - intrinsic * intrinsic, 0.0)
234
+ extrinsic = float(np.sqrt(residual_sq))
235
+ return {"intrinsic": intrinsic, "extrinsic": extrinsic}
236
+
181
237
  def _polygon_to_shape_vector(
182
238
  self, polygon: Polygon, pattern: Pattern
183
239
  ) -> np.ndarray: