hypertopos 0.2.2__tar.gz → 0.3.1__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 (211) hide show
  1. {hypertopos-0.2.2 → hypertopos-0.3.1}/CHANGELOG.md +61 -0
  2. {hypertopos-0.2.2 → hypertopos-0.3.1}/LICENSE.md +1 -1
  3. {hypertopos-0.2.2 → hypertopos-0.3.1}/PKG-INFO +3 -5
  4. {hypertopos-0.2.2 → hypertopos-0.3.1}/README.md +2 -2
  5. {hypertopos-0.2.2 → hypertopos-0.3.1}/ROADMAP.md +54 -18
  6. {hypertopos-0.2.2 → hypertopos-0.3.1}/SECURITY.md +1 -1
  7. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/api-reference.md +4 -4
  8. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/architecture.md +1 -1
  9. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/concepts.md +10 -0
  10. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/configuration.md +1 -0
  11. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/data-format.md +2 -1
  12. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/__init__.py +1 -1
  13. hypertopos-0.3.1/hypertopos/builder/_scatter.py +72 -0
  14. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/builder/_stats.py +25 -1
  15. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/builder/builder.py +632 -213
  16. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/builder/derived.py +23 -19
  17. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/cli/build.py +9 -1
  18. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/engine/aggregation.py +282 -523
  19. hypertopos-0.3.1/hypertopos/engine/fdr.py +89 -0
  20. hypertopos-0.3.1/hypertopos/engine/lance_sql_agg.py +798 -0
  21. hypertopos-0.3.1/hypertopos/engine/selection.py +102 -0
  22. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/navigation/navigator.py +281 -62
  23. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/navigation/scanner.py +41 -64
  24. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/storage/reader.py +34 -0
  25. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/storage/writer.py +35 -3
  26. {hypertopos-0.2.2 → hypertopos-0.3.1}/pyproject.toml +3 -6
  27. hypertopos-0.3.1/tests/conftest.py +89 -0
  28. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_aggregation_engine.py +0 -66
  29. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_builder.py +28 -6
  30. hypertopos-0.3.1/tests/test_builder_chunking.py +93 -0
  31. hypertopos-0.3.1/tests/test_builder_contagion.py +99 -0
  32. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_builder_derived.py +18 -3
  33. hypertopos-0.3.1/tests/test_builder_rolling_z.py +83 -0
  34. hypertopos-0.3.1/tests/test_builder_scatter.py +177 -0
  35. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_calibration.py +10 -8
  36. hypertopos-0.3.1/tests/test_fdr.py +128 -0
  37. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_fts.py +15 -5
  38. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_geometric_path.py +7 -8
  39. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_incremental.py +4 -3
  40. hypertopos-0.3.1/tests/test_lance_sql_agg.py +548 -0
  41. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_navigation.py +34 -21
  42. hypertopos-0.3.1/tests/test_navigator_fdr_select.py +540 -0
  43. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_scanner.py +45 -0
  44. hypertopos-0.3.1/tests/test_selection.py +131 -0
  45. hypertopos-0.2.2/hypertopos/engine/datafusion_agg.py +0 -602
  46. hypertopos-0.2.2/hypertopos/engine/subprocess_agg.py +0 -742
  47. hypertopos-0.2.2/tests/conftest.py +0 -83
  48. hypertopos-0.2.2/tests/test_datafusion_agg.py +0 -788
  49. hypertopos-0.2.2/tests/test_subprocess_agg.py +0 -961
  50. {hypertopos-0.2.2 → hypertopos-0.3.1}/.github/workflows/publish-pypi.yml +0 -0
  51. {hypertopos-0.2.2 → hypertopos-0.3.1}/.gitignore +0 -0
  52. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/README.md +0 -0
  53. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/LICENSE +0 -0
  54. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/README.md +0 -0
  55. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/README.md +0 -0
  56. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/SUMMARY.md +0 -0
  57. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-exp-a-report.md +0 -0
  58. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-exp-a-scoring.md +0 -0
  59. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-exp-b-report.md +0 -0
  60. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-exp-b-scoring.md +0 -0
  61. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-settings.md +0 -0
  62. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-exp-a-report.md +0 -0
  63. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-exp-a-scoring.md +0 -0
  64. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-exp-b-report.md +0 -0
  65. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-exp-b-scoring.md +0 -0
  66. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-settings.md +0 -0
  67. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-exp-a-report.md +0 -0
  68. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-exp-a-scoring.md +0 -0
  69. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-exp-b-report.md +0 -0
  70. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-exp-b-scoring.md +0 -0
  71. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-settings.md +0 -0
  72. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-exp-a-report.md +0 -0
  73. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-exp-a-scoring.md +0 -0
  74. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-exp-b-report.md +0 -0
  75. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-exp-b-scoring.md +0 -0
  76. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-settings.md +0 -0
  77. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-exp-a-report.md +0 -0
  78. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-exp-a-scoring.md +0 -0
  79. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-exp-b-report.md +0 -0
  80. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-exp-b-scoring.md +0 -0
  81. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-settings.md +0 -0
  82. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-exp-a-report.md +0 -0
  83. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-exp-a-scoring.md +0 -0
  84. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-exp-b-report.md +0 -0
  85. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-exp-b-scoring.md +0 -0
  86. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-settings.md +0 -0
  87. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/sphere/prepare_accounts.py +0 -0
  88. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/sphere/prepare_cpty_banks.py +0 -0
  89. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/sphere/prepare_districts.py +0 -0
  90. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/sphere/prepare_loan_accounts.py +0 -0
  91. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/sphere/prepare_operations.py +0 -0
  92. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/sphere/prepare_orders.py +0 -0
  93. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/sphere/prepare_transactions.py +0 -0
  94. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/sphere/prepare_tx_types.py +0 -0
  95. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/berka/sphere/sphere.yaml +0 -0
  96. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/ibm-aml/README.md +0 -0
  97. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/ibm-aml/_layer2_cross_eval.py +0 -0
  98. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/ibm-aml/_layer3_cross_eval.py +0 -0
  99. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_accounts.py +0 -0
  100. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_currencies.py +0 -0
  101. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_payment_formats.py +0 -0
  102. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_transactions.py +0 -0
  103. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/ibm-aml/hi_small_sphere/sphere.yaml +0 -0
  104. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/ibm-aml/layer1_passive_scan.py +0 -0
  105. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/ibm-aml/layer2_xgboost_sweep.py +0 -0
  106. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/ibm-aml/layer3_typology_filter.py +0 -0
  107. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/ibm-aml/li_small_sphere/prepare_accounts.py +0 -0
  108. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/ibm-aml/li_small_sphere/prepare_currencies.py +0 -0
  109. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/ibm-aml/li_small_sphere/prepare_payment_formats.py +0 -0
  110. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/ibm-aml/li_small_sphere/prepare_transactions.py +0 -0
  111. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/ibm-aml/li_small_sphere/sphere.yaml +0 -0
  112. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/ibm-aml/passive-scan-calibration.md +0 -0
  113. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/nyc-taxi/README.md +0 -0
  114. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/nyc-taxi/results/0.1.0/SUMMARY.md +0 -0
  115. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-a-report.md +0 -0
  116. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-a-scoring.md +0 -0
  117. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-b-report.md +0 -0
  118. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-b-scoring.md +0 -0
  119. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-c-report.md +0 -0
  120. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-settings.md +0 -0
  121. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-a-report.md +0 -0
  122. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-a-scoring.md +0 -0
  123. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-b-report.md +0 -0
  124. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-b-scoring.md +0 -0
  125. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-c-report.md +0 -0
  126. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-settings.md +0 -0
  127. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-a-report.md +0 -0
  128. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-a-scoring.md +0 -0
  129. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-b-report.md +0 -0
  130. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-b-scoring.md +0 -0
  131. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-c-report.md +0 -0
  132. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-settings.md +0 -0
  133. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/nyc-taxi/sphere/prepare_trips.py +0 -0
  134. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmark/nyc-taxi/sphere/sphere.yaml +0 -0
  135. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/benchmarks.md +0 -0
  136. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/images/build-pipeline.svg +0 -0
  137. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/images/core-objects.svg +0 -0
  138. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/images/cutting-plane.svg +0 -0
  139. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/images/directory-layout.svg +0 -0
  140. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/images/hypertopos-overview.svg +0 -0
  141. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/images/model-lifecycle.svg +0 -0
  142. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/images/navigation-primitives.svg +0 -0
  143. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/images/three-scales.svg +0 -0
  144. {hypertopos-0.2.2 → hypertopos-0.3.1}/docs/quickstart.md +0 -0
  145. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/builder/__init__.py +0 -0
  146. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/builder/_writer.py +0 -0
  147. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/builder/mapping.py +0 -0
  148. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/cli/__init__.py +0 -0
  149. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/cli/__main__.py +0 -0
  150. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/cli/info.py +0 -0
  151. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/cli/main.py +0 -0
  152. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/cli/schema.py +0 -0
  153. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/cli/sources.py +0 -0
  154. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/engine/__init__.py +0 -0
  155. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/engine/calibration.py +0 -0
  156. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/engine/chains.py +0 -0
  157. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/engine/composition.py +0 -0
  158. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/engine/forecast.py +0 -0
  159. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/engine/geometry.py +0 -0
  160. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/engine/invalidation.py +0 -0
  161. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/engine/investigation.py +0 -0
  162. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/model/__init__.py +0 -0
  163. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/model/manifest.py +0 -0
  164. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/model/objects.py +0 -0
  165. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/model/sphere.py +0 -0
  166. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/navigation/__init__.py +0 -0
  167. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/sphere.py +0 -0
  168. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/storage/__init__.py +0 -0
  169. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/storage/_schemas.py +0 -0
  170. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/storage/cache.py +0 -0
  171. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/utils/__init__.py +0 -0
  172. {hypertopos-0.2.2 → hypertopos-0.3.1}/hypertopos/utils/arrow.py +0 -0
  173. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/.gitkeep +0 -0
  174. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/fixtures/generate_fixtures.py +0 -0
  175. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_arrow_utils.py +0 -0
  176. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_builder_alias.py +0 -0
  177. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_builder_edges.py +0 -0
  178. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_cache.py +0 -0
  179. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_chains.py +0 -0
  180. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_cli.py +0 -0
  181. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_cli_alias.py +0 -0
  182. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_cli_sources.py +0 -0
  183. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_compaction.py +0 -0
  184. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_composition.py +0 -0
  185. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_continuous_event_dims.py +0 -0
  186. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_data_quality_prop.py +0 -0
  187. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_detection.py +0 -0
  188. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_edge_table.py +0 -0
  189. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_engine.py +0 -0
  190. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_forecast.py +0 -0
  191. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_geometry_stats.py +0 -0
  192. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_hybrid_search.py +0 -0
  193. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_invalidation.py +0 -0
  194. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_investigation.py +0 -0
  195. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_mapping.py +0 -0
  196. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_model.py +0 -0
  197. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_mvcc_session.py +0 -0
  198. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_navigator_alerts.py +0 -0
  199. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_observability.py +0 -0
  200. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_precision_stack.py +0 -0
  201. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_reputation.py +0 -0
  202. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_row_id_cache.py +0 -0
  203. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_sphere.py +0 -0
  204. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_sphere_overview_perf.py +0 -0
  205. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_stats_guard.py +0 -0
  206. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_storage.py +0 -0
  207. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_streaming_reads.py +0 -0
  208. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_temporal_centroids.py +0 -0
  209. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_trajectory_ann.py +0 -0
  210. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_witness_cohort.py +0 -0
  211. {hypertopos-0.2.2 → hypertopos-0.3.1}/tests/test_witness_set.py +0 -0
@@ -5,6 +5,67 @@ All notable changes to hypertopos will be documented in this file.
5
5
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
  Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.1] — 2026-04-12
9
+
10
+ ### Added
11
+
12
+ - Benjamini-Hochberg FDR control on attract primitives. `fdr_alpha` parameter on `pi5_attract_anomaly`, `pi6_attract_boundary`, `pi7_attract_hub`, and `pi9_attract_drift` applies BH multiple-testing correction to empirical-null p-values derived from delta rank percentile. Entities with `q_value > alpha` are removed before the top-N step. Guarantees `E[FDR] <= alpha`. Each retained entity carries a `q_value` — the minimum alpha at which it would still be retained. Off by default; legacy behavior preserved when omitted.
13
+ - Diverse selection via submodular facility location. `select="diverse"` on the same four primitives replaces top-N ranking with a lazy-greedy maximisation of the facility location objective over pairwise cosine similarity of delta vectors. Achieves `(1 - 1/e)` approximation to the optimal K-subset. Each selected entity reports a `representativeness` count — how many population members are nearest to it among the selected set. Covers the geometric space of the result set instead of clustering around a single extreme region.
14
+ - `engine/fdr.py` module: `benjamini_hochberg`, `empirical_p_values_from_rank`, `q_values_from_p_values` — pure NumPy, no state, no I/O.
15
+ - `engine/selection.py` module: `lazy_greedy_facility_location`, `compute_similarity_matrix` — pure NumPy, Nemhauser-Wolsey-Fisher optimality guarantee.
16
+
17
+ ### Changed
18
+
19
+ - Build pipeline: derived dimension scatter loops replaced with Arrow `pc.index_in` + numpy fancy indexing. Eliminates per-row Python iteration in both static (`compute_derived_batch`) and temporal (`_precompute_shape_tensor`) build paths.
20
+ - Build pipeline: contagion stats computation (`_build_contagion_stats`) rewritten from Python loops over edge table to Arrow group_by + join. Eliminates O(E+N) Python iterations.
21
+ - Build pipeline: rolling z-score (`_compute_max_rolling_z`) uses Welford online algorithm. Reduces time complexity from O(n_buckets² × N × D) to O(n_buckets × N × D) and memory from O(N × n_buckets × D) to O(N × D).
22
+ - Build pipeline: temporal Arrow assembly uses single numpy reshape instead of per-bucket table construction loop.
23
+ - Build pipeline: graph dims temporal computation uses pre-sorted event table with `searchsorted` for O(1) per-bucket slicing instead of O(E) scan per bucket.
24
+ - Build pipeline: `build_temporal` adaptively chunks entities when the shape tensor would exceed available RAM. Auto-detects memory via platform APIs, falls back to 4 GB budget. Single-pass path (zero overhead) when tensor fits.
25
+
26
+ ### Fixed
27
+
28
+ - `empirical_p_values_from_rank` used a `1/N` floor where N was the input array length. When the input was a small `top_n` slice (e.g. 5 entities), the floor clipped all p-values to 0.2, causing BH to reject everything at `alpha=0.05`. Now uses a fixed `1e-10` floor — the rank percentile is already computed against the full population, so the floor only needs to prevent exact zero for numerical stability.
29
+ - `π7_attract_hub` and `π9_attract_drift` FDR computed p-values as `(N-i)/N` where N was the `top_n` result count (e.g. 10), making the best possible p-value `1/top_n` — mathematically unable to pass BH at any `alpha < 1/top_n`. Now uses population-level rank percentiles: hubs use the empirical CDF from the full scores array, drift uses the rank position relative to the total pre-truncation population.
30
+
31
+ ## [0.3.0] — 2026-04-12
32
+
33
+ > **Theme:** Lance 3.x perf upgrade. Aggregate engine moves from a persistent subprocess worker plus an optional external DataFusion package to the Lance scanner's built-in DataFusion executor via `LanceDataset.sql(...)`. The build-time contagion stats table replaces the runtime edge-table replay in the graph contagion scanner. Format 2.2 is the new write default.
34
+ >
35
+ > **Mandatory rebuild for spheres built before this release.** See the Migration section below.
36
+
37
+ ### Changed
38
+
39
+ - `pylance` minimum bumped to 4.x. New writes target Lance format 2.2 with structural-decode parallelism, BSS float compression, cascading codecs, and block-level RLE on top of the previous baseline. Older spheres written with format 2.0 / 2.1 remain readable transparently.
40
+ - The aggregate engine now pushes GROUP BY computation directly into the Lance scanner. The new `lance_sql_agg` module hosts `aggregate_count`, `aggregate_metric` (sum / avg / min / max), `aggregate_filtered_metric`, `aggregate_pivot`, `aggregate_property`, `aggregate_percentile`, and `find_anomalies`. Each helper streams the columns it needs from the geometry / event-line / property-line Lance datasets through `LanceDataset.sql(...)` and finishes the join + aggregate in pyarrow on the small post-projection result. The full geometry table is never loaded into Python memory and the persistent subprocess worker is no longer involved for any of these paths.
41
+ - Test fixture cloning uses `lance.LanceDataset.shallow_clone` for every Lance dataset directory inside a sphere tree, with a thin `clone_sphere` helper in `tests/conftest.py` that walks the directory and falls back to `shutil.copy2` for non-Lance files. Shallow clone copies only metadata and references — measurably faster on Windows where deep `copytree` over a sphere built from many small Lance fragment files is dominated by per-file NTFS overhead.
42
+
43
+ ### Added
44
+
45
+ - `GDSBuilder` rejects patterns with zero declared dimensions (no relations, no event dimensions, no derived dimensions, no tracked properties) at validation time. A pattern with width-0 delta vectors had no meaningful geometry to compute even before this release; format 2.0 silently allowed the construct, format 2.1+ refuses to encode the resulting `fixed_size_list[0]` columns at all. The validation surfaces a typed error up front instead of letting the call descend into an opaque write-time panic.
46
+ - Builder precomputes per-entity contagion statistics for every pattern with an edge table and writes them to `_gds_meta/contagion_stats/{pattern_id}.lance` with a BTREE index on `primary_key`. The table holds `(primary_key, neighbor_count, anomalous_neighbor_count, contagion_ratio)` snapshotted at build time against the just-calibrated anchor geometry.
47
+
48
+ ### Fixed
49
+
50
+ - `_scan_graph` (the graph contagion source feeding `passive_scan` and `composite_risk`) reads the precomputed `_gds_meta/contagion_stats/{pattern_id}.lance` table directly and threshold-filters inline. The legacy edge-table-replay path is gone; spheres built before 0.3.0 must be rebuilt to get graph contagion hits.
51
+ - `contagion_score` anchor pattern resolution now correctly resolves via sibling lines when the anchor pattern is not directly associated with the target entity's line. Previously crashed with `KeyError` on spheres where the anchor pattern entity line differed from the navigated line.
52
+ - Edge table auto-detect excludes metadata columns (`created_at`, `changed_at`, `version`) from the timestamp type-based fallback, preventing incorrect timestamp column selection on entity lines with versioning columns. Amount candidate list widened to cover `fare_amount`, `total_amount`, `amount_received`, `amount_paid`.
53
+
54
+ ### Security
55
+
56
+ - `lance_sql_agg` validates user-controlled SQL inputs up front. Lance's `LanceDataset.sql(...)` does not expose parameter binding, so values and identifiers are inlined into the query string. Filter values (entity primary keys) are escaped with `_escape_sql_string`, which doubles single quotes and rejects backslash and ASCII control characters. Column-name identifiers (`metric_col`, `pivot_field`, `prop_name`) are validated against `^[A-Za-z_][A-Za-z0-9_]*$` via `_validate_sql_identifier` before being inlined into SQL strings — anything outside that pattern raises immediately. Both helpers fail loud rather than silently produce wrong results or pass injected SQL through to the parser.
57
+
58
+ ### Removed
59
+
60
+ - `hypertopos.engine.subprocess_agg` (the persistent subprocess worker and its JSON command protocol) is deleted; every entry point it served is now routed through `lance_sql_agg`.
61
+ - `hypertopos.engine.datafusion_agg` (the in-memory external-DataFusion fast path for count / metric / pivot / gbp / percentile) is deleted along with the optional `datafusion` extra in `pyproject.toml`. Lance 4.x's built-in DataFusion executor covers every query the external package was carrying.
62
+ - The matching `tests/test_subprocess_agg.py` and `tests/test_datafusion_agg.py` are deleted. Aggregate semantics are exercised through `tests/test_aggregation_engine.py` and the MCP tool tests.
63
+
64
+ ### Migration
65
+
66
+ - **Rebuild required.** Spheres built before 0.3.0 are still openable but degrade gracefully on graph contagion — the precomputed `contagion_stats` table is created at build time only, and the runtime scanner returns zero hits without it. Rebuild with `hypertopos build --config sphere.yaml --force`. There are no in-place upgrade paths.
67
+ - The optional `datafusion` package is no longer a dependency of any aggregate path. If your environment installed it via `pip install hypertopos[datafusion]`, that extra now resolves to nothing — you can drop it from your install command.
68
+
8
69
  ## [0.2.2] — 2026-04-11
9
70
 
10
71
  ### 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-04-11
29
+ **Change Date:** 2029-04-12
30
30
 
31
31
  **Change License:** Apache License, Version 2.0
32
32
 
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypertopos
3
- Version: 0.2.2
4
- Summary: Navigate your data like space.
3
+ Version: 0.3.1
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
7
7
  Project-URL: Repository, https://github.com/hypertopos/hypertopos-py
@@ -12,10 +12,8 @@ License-File: LICENSE.md
12
12
  Requires-Python: >=3.12
13
13
  Requires-Dist: numpy>=1.26
14
14
  Requires-Dist: pyarrow>=14.0
15
- Requires-Dist: pylance>=0.20
15
+ Requires-Dist: pylance<5.0,>=4.0
16
16
  Requires-Dist: pyyaml>=6.0
17
- Provides-Extra: datafusion
18
- Requires-Dist: datafusion>=40.0; extra == 'datafusion'
19
17
  Provides-Extra: dev
20
18
  Requires-Dist: mypy; extra == 'dev'
21
19
  Requires-Dist: pytest; extra == 'dev'
@@ -6,9 +6,9 @@
6
6
  [![License: BSL 1.1](https://img.shields.io/badge/license-BSL%201.1-orange.svg)](LICENSE.md)
7
7
  [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.19482069.svg)](https://doi.org/10.5281/zenodo.19482069)
8
8
  [![PyArrow](https://img.shields.io/badge/format-PyArrow-red.svg)](https://arrow.apache.org/docs/python/)
9
- [![Lance](https://img.shields.io/badge/storage-Lance-blueviolet.svg)](https://github.com/lancedb/lancedb)
9
+ [![Lance](https://img.shields.io/badge/storage-Lance-blueviolet.svg)](https://github.com/lance-format/lance)
10
10
  [![MCP](https://img.shields.io/badge/MCP-compatible-green.svg)](https://modelcontextprotocol.io)
11
- [![Version](https://img.shields.io/badge/version-0.2.2-blue.svg)](pyproject.toml)
11
+ [![Version](https://img.shields.io/badge/version-0.3.0-blue.svg)](pyproject.toml)
12
12
 
13
13
  **hypertopos** transforms relational data into navigable geometric space.
14
14
 
@@ -2,7 +2,19 @@
2
2
 
3
3
  > Planned direction. Priorities may change based on feedback.
4
4
 
5
- ## Current: 0.2.2
5
+ ## Current: 0.3.1
6
+
7
+ FDR control, diverse anomaly summarisation, and build pipeline optimization — Benjamini-Hochberg multiple-testing correction, submodular facility location, vectorized build with adaptive memory chunking. See CHANGELOG.
8
+
9
+ ---
10
+
11
+ ## 0.3.0
12
+
13
+ Lance 3.x perf upgrade — aggregate engine rewritten around Lance SQL, precomputed contagion stats, format 2.2, contagion anchor resolution fix, edge table auto-detect fix. See CHANGELOG.
14
+
15
+ ---
16
+
17
+ ## 0.2.2
6
18
 
7
19
  As-of graph reconstruction across edge-table graph primitives + latency fix on `detect_cross_pattern_discrepancy`. See CHANGELOG.
8
20
 
@@ -26,33 +38,57 @@ First public release — full GDS stack, π1–π12, builder, MCP server, valida
26
38
 
27
39
  ---
28
40
 
29
- ## Plan: 0.3.0
41
+ ## Plan: 0.3.1
30
42
 
31
- **Storage / performance:**
32
- - Upgrade `pylance 2.0.1 → 3.0.1` — format 2.2, ~300% faster scans, LZ4/zstd compression
33
- - Replace `shutil.copytree` with `lance.dataset.clone()` in test fixtures (O(1) metadata clone)
34
- - Builder: incremental rebuild (geometry-only without `--force` wipe)
43
+ Two production-quality improvements on top of the 0.3.0 storage layer:
35
44
 
36
- **Code refactoring:**
37
- - Break up oversized modules into smaller, domain-focused components
38
- - Reduce coupling between core layers by replacing private cross-layer access with explicit interfaces where practical
39
- - Tighten error handling in hot paths so failures are surfaced more consistently
40
- - Consolidate repeated orchestration logic into shared helpers instead of duplicating it across modules
41
- - Make the refactor pass incremental: preserve behavior, improve structure, then revisit deeper architectural boundaries
45
+ - **FDR control on anomaly queries** — Benjamini–Hochberg `fdr_alpha` parameter + per-row `q_value` column on `find_anomalies` and friends. Replaces unbounded "top-K by `delta_norm`" with a set whose false-discovery rate is bounded by α.
46
+ - **Diverse-coverage anomaly summarisation** — `select="diverse"` option using lazy-greedy submodular maximisation, plus a `representativeness` column ("this 1 anomaly stands in for 38 cases").
47
+ - **Build pipeline optimization** — vectorized scatter (Arrow `pc.index_in` + numpy), Welford rolling z-score, Arrow-native contagion stats, adaptive memory chunking for temporal build, pre-sorted graph dims bucketing.
42
48
 
43
- **PassiveScanner evolution:**
44
- - Native temporal source support for direct temporal inputs, without requiring manual dataset plumbing in benchmark scripts
45
- - Optional weighted scoring mode that uses continuous intensity instead of binary counts
46
- - **SphereProfiler** — autonomous sphere scanner that profiles all patterns, runs calibration sweeps across source combinations, proposes optimal PassiveScanner composition for Layer 1 surveillance. Core loop: enumerate patterns → single-source anomaly scans → greedy multi-source combination → ranked composition report. No labeled GT needed; optional GT file unlocks supervised calibration.
49
+ ---
47
50
 
48
- **Anomaly detection quality:**
49
- - Confidence scoring, robust estimators, multi-scale resolution — improve anomaly precision and reduce false positives on heavy-tail and multi-modal populations
51
+ ## Plan: 0.3.2
52
+
53
+ TBD.
54
+
55
+ ---
56
+
57
+ ## Plan: 0.4.0
58
+
59
+ TBD.
50
60
 
51
61
  ---
52
62
 
53
63
  ## Future
54
64
 
65
+ **Detection quality**
66
+ - Edge-derived dimensions + temporal motif matcher
67
+ - Confidence scoring, robust estimators, multi-scale resolution — improve anomaly precision and reduce false positives on heavy-tail and multi-modal populations
68
+ - Lazy chain geometry — on-demand chain delta vectors via sampled population calibration; supplements the build-time `chain_lines` path
69
+
70
+ **Builder evolution**
71
+ - Incremental rebuild — geometry-only without `--force` wipe
72
+ - Generalized (d+m+g+t+s) dimension blocks — geographic / metric / semantic dimension support
73
+
74
+ **PassiveScanner evolution**
75
+ - Native temporal source support for direct temporal inputs, without requiring manual dataset plumbing in benchmark scripts
76
+ - Optional weighted scoring mode that uses continuous intensity instead of binary counts
77
+ - **SphereProfiler** — autonomous sphere scanner that profiles all patterns, runs calibration sweeps across source combinations, proposes optimal PassiveScanner composition for Layer 1 surveillance. Core loop: enumerate patterns → single-source anomaly scans → greedy multi-source combination → ranked composition report. No labeled GT needed; optional GT file unlocks supervised calibration.
78
+
79
+ **Code refactoring**
80
+ - Break up oversized modules into smaller, domain-focused components
81
+ - Reduce coupling between core layers by replacing private cross-layer access with explicit interfaces where practical
82
+ - Tighten error handling in hot paths so failures are surfaced more consistently
83
+ - Consolidate repeated orchestration logic into shared helpers instead of duplicating it across modules
84
+ - Incremental refactor: preserve behavior, improve structure, then revisit deeper architectural boundaries
85
+
86
+ **Cross-sphere capabilities**
55
87
  - Cross-sphere comparison — dimensionless metrics across independently calibrated coordinate spaces
56
88
  - What-if analysis — hypothetical edge changes producing modified coordinate vectors
89
+
90
+ **Enterprise / governance**
57
91
  - Dimension access control — per-agent visibility constraints on delta dimensions
92
+
93
+ **Tooling**
58
94
  - Runtime latency benchmarks in package docs
@@ -1,6 +1,6 @@
1
1
  # Security
2
2
 
3
- ## Current version: 0.2.2
3
+ ## Current version: 0.3.0
4
4
 
5
5
  hypertopos runs locally. No network services, no auth layer, no multi-tenancy.
6
6
 
@@ -103,9 +103,9 @@ with sphere.session("agent-1") as session:
103
103
 
104
104
  | Primitive | Method | What it does | Returns |
105
105
  |-----------|--------|--------------|---------|
106
- | π5 | `π5_attract_anomaly(pattern_id, radius, top_n)` | Find most anomalous polygons | `(list[Polygon], int, list, dict)` |
107
- | π6 | `π6_attract_boundary(alias_id, pattern_id, direction, top_n)` | Find entities nearest to alias cutting plane | `list[(Polygon, float)]` |
108
- | π7 | `π7_attract_hub(pattern_id, top_n, line_id_filter)` | Find entities with highest connectivity | `list[(str, int, float)]` |
106
+ | π5 | `π5_attract_anomaly(pattern_id, radius, top_n, fdr_alpha, fdr_method, select)` | Find most anomalous polygons | `(list[Polygon], int, list, dict)` |
107
+ | π6 | `π6_attract_boundary(alias_id, pattern_id, direction, top_n, fdr_alpha, fdr_method, select)` | Find entities nearest to alias cutting plane | `list[(Polygon, float)]` |
108
+ | π7 | `π7_attract_hub(pattern_id, top_n, line_id_filter, fdr_alpha, fdr_method, select)` | Find entities with highest connectivity | `list[(str, int, float)]` |
109
109
  | π7+ | `π7_attract_hub_and_stats(pattern_id, top_n, line_id_filter)` | Hub ranking + population hub score statistics in one scan | `(list, dict)` |
110
110
  | π8 | `π8_attract_cluster(pattern_id, n_clusters, top_n, sample_size)` | Discover geometric archetypes via k-means++ | `list[dict]` |
111
111
 
@@ -113,7 +113,7 @@ with sphere.session("agent-1") as session:
113
113
 
114
114
  | Primitive | Method | What it does | Returns |
115
115
  |-----------|--------|--------------|---------|
116
- | π9 | `π9_attract_drift(pattern_id, top_n, sample_size)` | Find entities with highest temporal drift | `list[dict]` |
116
+ | π9 | `π9_attract_drift(pattern_id, top_n, sample_size, fdr_alpha, fdr_method, select)` | Find entities with highest temporal drift | `list[dict]` |
117
117
  | π10 | `π10_attract_trajectory(primary_key, pattern_id, top_n)` | Find entities with similar temporal trajectory | `list[dict]` |
118
118
  | π11 | `π11_attract_population_compare(pattern_id, window_a_from, window_a_to, window_b_from, window_b_to)` | Compare population geometry between two time windows | `dict` |
119
119
  | π12 | `π12_attract_regime_change(pattern_id, timestamp_from, timestamp_to)` | Detect population geometry regime shifts | `list[dict]` |
@@ -24,7 +24,7 @@ The core package is split into a few layers:
24
24
 
25
25
  - **Model**: sphere, line, pattern, alias, point, edge, polygon, solid, manifest, contract
26
26
  - **Storage**: readers, writers, caches, and physical layout handling
27
- - **Engine**: geometry, anomaly scoring, similarity, drift, forecasting, aggregation, calibration tracking
27
+ - **Engine**: geometry, anomaly scoring, similarity, drift, forecasting, aggregation (Lance SQL pushdown via `lance_sql_agg`), calibration tracking
28
28
  - **Navigation**: stateful step-by-step movement through the sphere
29
29
  - **PassiveScanner**: multi-source batch screening over navigation primitives
30
30
  - **Builder**: transforms source data into a complete sphere
@@ -58,6 +58,16 @@ where μ = E[s] is the population mean, σ = max(std(s), ε) the clamped standar
58
58
  - **Cross-sphere comparison** *(planned)* — dimensionless scalar metrics from independently calibrated coordinate spaces enable comparison across separate data systems
59
59
  - **What-if analysis** *(planned)* — hypothetical edge changes produce a modified coordinate vector showing the geometric effect
60
60
 
61
+ ## Statistical Guarantees
62
+
63
+ Two opt-in post-processing stages can be applied to any attract primitive that scans a population (pi5, pi6, pi7, pi9). Both are off by default — omitting them preserves the legacy behavior exactly.
64
+
65
+ **FDR control.** When `fdr_alpha` is set, the result set passes through a Benjamini-Hochberg (BH) procedure. Each entity's empirical-null p-value is derived from its population rank percentile: `p = 1 - rank_pct / 100`, where `rank_pct` is the entity's position in the full population sorted by the primitive's ranking metric (delta norm for pi5/pi6, hub score for pi7, displacement for pi9). The BH procedure guarantees `E[FDR] <= alpha` under independence or positive regression dependency (PRDS). Every retained entity carries a q-value — the minimum alpha at which it would still be retained. Entities with `q_value > alpha` are removed before the top-N / selection step.
66
+
67
+ **Diverse selection.** When `select="diverse"`, the top-N step is replaced by a lazy-greedy facility location algorithm that maximizes a monotone submodular objective over pairwise cosine similarity of delta vectors. The greedy selection achieves a `(1 - 1/e)` approximation to the optimal K-subset. Each selected entity reports a representativeness count — how many population members are closest to it among the selected set. The result is K entities that cover the geometric space of the result set rather than clustering around a single extreme region.
68
+
69
+ Both stages compose: `fdr_alpha=0.05, select="diverse"` first filters to FDR-controlled discoveries, then picks the K most geometrically diverse among them.
70
+
61
71
  ## Three Scales
62
72
 
63
73
  ![Three Scales](images/three-scales.svg)
@@ -871,6 +871,7 @@ hypertopos build --config sphere.yaml --force --verbose
871
871
  The builder parallelizes where possible:
872
872
  - **Points write** — parallel across lines (ThreadPoolExecutor, up to 4 workers)
873
873
  - **Geometry build** — parallel across patterns (one thread per pattern)
874
+ - **Temporal build** — parallel across patterns; adaptive memory chunking when the shape tensor exceeds available RAM (auto-detected via platform API, 4 GB fallback)
874
875
 
875
876
  ### Build timing
876
877
 
@@ -17,7 +17,8 @@ gds_{sphere_id}/
17
17
  │ ├── geometry_stats/ # precomputed population summaries
18
18
  │ ├── trajectory/ # ANN index for trajectory similarity search
19
19
  │ ├── temporal_centroids/ # cached population centroids per time window
20
- │ └── edge_stats/ # per-event-pattern edge table summary cache (row count, unique from/to, ts/amount range)
20
+ │ ├── edge_stats/ # per-event-pattern edge table summary cache (row count, unique from/to, ts/amount range)
21
+ │ └── contagion_stats/ # per-pattern (primary_key, neighbor_count, anomalous_neighbor_count, contagion_ratio) — feeds the graph contagion scanner directly, BTREE-indexed on primary_key
21
22
  ├── points/
22
23
  │ ├── {line_id}/v={n}/
23
24
  │ │ └── data.lance # entity records for this line version
@@ -16,4 +16,4 @@ __all__ = [
16
16
  "GDSBuilder",
17
17
  "RelationSpec",
18
18
  ]
19
- __version__ = "0.2.2"
19
+ __version__ = "0.3.0"
@@ -0,0 +1,72 @@
1
+ # Copyright (C) 2026 Karol Kędzia
2
+ # Licensed under the Business Source License 1.1 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # See LICENSE.md in the repository root for full terms.
5
+ """Vectorized Arrow→tensor scatter utilities.
6
+
7
+ Replaces Python-level row iteration with Arrow pc.index_in + numpy
8
+ fancy indexing. Used by both static (compute_derived_batch) and
9
+ temporal (_precompute_shape_tensor) build paths.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import numpy as np
14
+ import pyarrow as pa
15
+ import pyarrow.compute as pc
16
+ from numpy.typing import NDArray
17
+
18
+
19
+ def vectorized_scatter(
20
+ tensor: NDArray[np.float32],
21
+ dim_idx: int,
22
+ edge_max: float,
23
+ anchor_keys_arr: pa.Array,
24
+ grouped_fk_col: pa.Array,
25
+ grouped_bucket_col: pa.Array,
26
+ grouped_values_col: pa.Array,
27
+ ) -> None:
28
+ if len(grouped_fk_col) == 0:
29
+ return
30
+
31
+ entity_idx_arr = pc.index_in(grouped_fk_col, anchor_keys_arr)
32
+ valid = pc.and_(pc.is_valid(entity_idx_arr), pc.is_valid(grouped_values_col))
33
+
34
+ entity_indices = entity_idx_arr.filter(valid).to_numpy(
35
+ zero_copy_only=False,
36
+ ).astype(np.intp)
37
+ bucket_indices = grouped_bucket_col.filter(valid).to_numpy(
38
+ zero_copy_only=False,
39
+ ).astype(np.intp)
40
+ values = grouped_values_col.filter(valid).to_numpy(
41
+ zero_copy_only=False,
42
+ ).astype(np.float64)
43
+
44
+ nan_ok = ~np.isnan(values)
45
+ entity_indices = entity_indices[nan_ok]
46
+ bucket_indices = bucket_indices[nan_ok]
47
+ values = np.clip(values[nan_ok], 0.0, edge_max) / edge_max
48
+
49
+ tensor[entity_indices, bucket_indices, dim_idx] = values.astype(np.float32)
50
+
51
+
52
+ def vectorized_scatter_1d(
53
+ result: NDArray[np.float64],
54
+ anchor_keys_arr: pa.Array,
55
+ grouped_fk_col: pa.Array,
56
+ grouped_values_col: pa.Array,
57
+ ) -> None:
58
+ if len(grouped_fk_col) == 0:
59
+ return
60
+
61
+ entity_idx_arr = pc.index_in(grouped_fk_col, anchor_keys_arr)
62
+ valid = pc.and_(pc.is_valid(entity_idx_arr), pc.is_valid(grouped_values_col))
63
+
64
+ entity_indices = entity_idx_arr.filter(valid).to_numpy(
65
+ zero_copy_only=False,
66
+ ).astype(np.intp)
67
+ values = grouped_values_col.filter(valid).to_numpy(
68
+ zero_copy_only=False,
69
+ ).astype(np.float64)
70
+
71
+ nan_ok = ~np.isnan(values)
72
+ result[entity_indices[nan_ok]] = values[nan_ok]
@@ -116,6 +116,7 @@ def compute_stats_grouped(
116
116
  shape_vectors: np.ndarray,
117
117
  group_ids: np.ndarray,
118
118
  anomaly_percentile: float = 95.0,
119
+ prop_dim_start: int | None = None,
119
120
  ) -> tuple[
120
121
  dict[str, tuple[np.ndarray, np.ndarray, np.ndarray, int]],
121
122
  np.ndarray,
@@ -123,6 +124,10 @@ def compute_stats_grouped(
123
124
  ]:
124
125
  """Compute per-group population statistics.
125
126
 
127
+ prop_dim_start: if set, apply SIGMA_EPS_PROP floor to sigma[prop_dim_start:]
128
+ for each group before computing deltas. Eliminates the need for a separate
129
+ re-run pass in the caller.
130
+
126
131
  Returns:
127
132
  group_results: {group_name: (mu, sigma, theta, population_size)}
128
133
  deltas: (N, D) float32 — z-scored against per-group mu/sigma
@@ -140,9 +145,12 @@ def compute_stats_grouped(
140
145
  group_shape = shape_vectors[mask]
141
146
 
142
147
  if len(group_shape) < 2:
143
- # Too few entities for meaningful stats — use global-like single point
144
148
  mu_g = group_shape.mean(axis=0).astype(np.float32)
145
149
  sigma_g = np.full(D, SIGMA_EPS, dtype=np.float32)
150
+ if prop_dim_start is not None:
151
+ sigma_g[prop_dim_start:] = np.maximum(
152
+ sigma_g[prop_dim_start:], SIGMA_EPS_PROP,
153
+ )
146
154
  theta_g = np.zeros(D, dtype=np.float32)
147
155
  deltas[mask] = ((group_shape - mu_g) / sigma_g).astype(np.float32)
148
156
  _masked = deltas[mask]
@@ -155,6 +163,22 @@ def compute_stats_grouped(
155
163
  mu_g, sigma_g, theta_g, group_deltas, group_norms, _ = compute_stats(
156
164
  group_shape, anomaly_percentile,
157
165
  )
166
+
167
+ if prop_dim_start is not None:
168
+ sigma_g[prop_dim_start:] = np.maximum(
169
+ sigma_g[prop_dim_start:], SIGMA_EPS_PROP,
170
+ )
171
+ group_deltas = ((group_shape - mu_g) / sigma_g).astype(np.float32)
172
+ group_norms = np.sqrt(
173
+ np.einsum('ij,ij->i', group_deltas, group_deltas),
174
+ ).astype(np.float32)
175
+ g_theta_scalar = (
176
+ float(np.percentile(group_norms, anomaly_percentile))
177
+ if len(group_norms) > 1 else 0.0
178
+ )
179
+ g_comp = g_theta_scalar / np.sqrt(D) if D > 0 else 0.0
180
+ theta_g = np.full(D, g_comp, dtype=np.float32)
181
+
158
182
  deltas[mask] = group_deltas
159
183
  delta_norms[mask] = group_norms
160
184
  group_results[gid_str] = (mu_g, sigma_g, theta_g, int(mask.sum()))