hypertopos 0.2.2__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. {hypertopos-0.2.2 → hypertopos-0.3.0}/CHANGELOG.md +38 -0
  2. {hypertopos-0.2.2 → hypertopos-0.3.0}/LICENSE.md +1 -1
  3. {hypertopos-0.2.2 → hypertopos-0.3.0}/PKG-INFO +3 -5
  4. {hypertopos-0.2.2 → hypertopos-0.3.0}/README.md +2 -2
  5. {hypertopos-0.2.2 → hypertopos-0.3.0}/ROADMAP.md +48 -18
  6. {hypertopos-0.2.2 → hypertopos-0.3.0}/SECURITY.md +1 -1
  7. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/architecture.md +1 -1
  8. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/data-format.md +2 -1
  9. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/__init__.py +1 -1
  10. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/builder/builder.py +153 -10
  11. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/cli/build.py +9 -1
  12. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/engine/aggregation.py +282 -523
  13. hypertopos-0.3.0/hypertopos/engine/lance_sql_agg.py +798 -0
  14. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/navigation/navigator.py +59 -57
  15. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/navigation/scanner.py +41 -64
  16. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/storage/reader.py +34 -0
  17. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/storage/writer.py +35 -3
  18. {hypertopos-0.2.2 → hypertopos-0.3.0}/pyproject.toml +3 -6
  19. hypertopos-0.3.0/tests/conftest.py +89 -0
  20. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_aggregation_engine.py +0 -66
  21. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_builder.py +28 -6
  22. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_builder_derived.py +18 -3
  23. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_calibration.py +10 -8
  24. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_fts.py +15 -5
  25. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_geometric_path.py +7 -8
  26. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_incremental.py +4 -3
  27. hypertopos-0.3.0/tests/test_lance_sql_agg.py +548 -0
  28. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_navigation.py +34 -21
  29. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_scanner.py +45 -0
  30. hypertopos-0.2.2/hypertopos/engine/datafusion_agg.py +0 -602
  31. hypertopos-0.2.2/hypertopos/engine/subprocess_agg.py +0 -742
  32. hypertopos-0.2.2/tests/conftest.py +0 -83
  33. hypertopos-0.2.2/tests/test_datafusion_agg.py +0 -788
  34. hypertopos-0.2.2/tests/test_subprocess_agg.py +0 -961
  35. {hypertopos-0.2.2 → hypertopos-0.3.0}/.github/workflows/publish-pypi.yml +0 -0
  36. {hypertopos-0.2.2 → hypertopos-0.3.0}/.gitignore +0 -0
  37. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/README.md +0 -0
  38. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/api-reference.md +0 -0
  39. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/LICENSE +0 -0
  40. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/README.md +0 -0
  41. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/README.md +0 -0
  42. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/SUMMARY.md +0 -0
  43. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-exp-a-report.md +0 -0
  44. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-exp-a-scoring.md +0 -0
  45. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-exp-b-report.md +0 -0
  46. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-exp-b-scoring.md +0 -0
  47. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-settings.md +0 -0
  48. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-exp-a-report.md +0 -0
  49. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-exp-a-scoring.md +0 -0
  50. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-exp-b-report.md +0 -0
  51. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-exp-b-scoring.md +0 -0
  52. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-settings.md +0 -0
  53. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-exp-a-report.md +0 -0
  54. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-exp-a-scoring.md +0 -0
  55. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-exp-b-report.md +0 -0
  56. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-exp-b-scoring.md +0 -0
  57. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-settings.md +0 -0
  58. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-exp-a-report.md +0 -0
  59. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-exp-a-scoring.md +0 -0
  60. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-exp-b-report.md +0 -0
  61. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-exp-b-scoring.md +0 -0
  62. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-settings.md +0 -0
  63. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-exp-a-report.md +0 -0
  64. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-exp-a-scoring.md +0 -0
  65. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-exp-b-report.md +0 -0
  66. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-exp-b-scoring.md +0 -0
  67. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-settings.md +0 -0
  68. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-exp-a-report.md +0 -0
  69. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-exp-a-scoring.md +0 -0
  70. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-exp-b-report.md +0 -0
  71. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-exp-b-scoring.md +0 -0
  72. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-settings.md +0 -0
  73. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/sphere/prepare_accounts.py +0 -0
  74. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/sphere/prepare_cpty_banks.py +0 -0
  75. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/sphere/prepare_districts.py +0 -0
  76. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/sphere/prepare_loan_accounts.py +0 -0
  77. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/sphere/prepare_operations.py +0 -0
  78. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/sphere/prepare_orders.py +0 -0
  79. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/sphere/prepare_transactions.py +0 -0
  80. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/sphere/prepare_tx_types.py +0 -0
  81. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/berka/sphere/sphere.yaml +0 -0
  82. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/ibm-aml/README.md +0 -0
  83. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/ibm-aml/_layer2_cross_eval.py +0 -0
  84. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/ibm-aml/_layer3_cross_eval.py +0 -0
  85. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_accounts.py +0 -0
  86. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_currencies.py +0 -0
  87. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_payment_formats.py +0 -0
  88. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_transactions.py +0 -0
  89. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/ibm-aml/hi_small_sphere/sphere.yaml +0 -0
  90. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/ibm-aml/layer1_passive_scan.py +0 -0
  91. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/ibm-aml/layer2_xgboost_sweep.py +0 -0
  92. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/ibm-aml/layer3_typology_filter.py +0 -0
  93. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/ibm-aml/li_small_sphere/prepare_accounts.py +0 -0
  94. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/ibm-aml/li_small_sphere/prepare_currencies.py +0 -0
  95. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/ibm-aml/li_small_sphere/prepare_payment_formats.py +0 -0
  96. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/ibm-aml/li_small_sphere/prepare_transactions.py +0 -0
  97. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/ibm-aml/li_small_sphere/sphere.yaml +0 -0
  98. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/ibm-aml/passive-scan-calibration.md +0 -0
  99. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/nyc-taxi/README.md +0 -0
  100. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/nyc-taxi/results/0.1.0/SUMMARY.md +0 -0
  101. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-a-report.md +0 -0
  102. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-a-scoring.md +0 -0
  103. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-b-report.md +0 -0
  104. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-b-scoring.md +0 -0
  105. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-c-report.md +0 -0
  106. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-settings.md +0 -0
  107. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-a-report.md +0 -0
  108. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-a-scoring.md +0 -0
  109. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-b-report.md +0 -0
  110. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-b-scoring.md +0 -0
  111. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-c-report.md +0 -0
  112. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-settings.md +0 -0
  113. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-a-report.md +0 -0
  114. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-a-scoring.md +0 -0
  115. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-b-report.md +0 -0
  116. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-b-scoring.md +0 -0
  117. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-c-report.md +0 -0
  118. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-settings.md +0 -0
  119. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/nyc-taxi/sphere/prepare_trips.py +0 -0
  120. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmark/nyc-taxi/sphere/sphere.yaml +0 -0
  121. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/benchmarks.md +0 -0
  122. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/concepts.md +0 -0
  123. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/configuration.md +0 -0
  124. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/images/build-pipeline.svg +0 -0
  125. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/images/core-objects.svg +0 -0
  126. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/images/cutting-plane.svg +0 -0
  127. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/images/directory-layout.svg +0 -0
  128. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/images/hypertopos-overview.svg +0 -0
  129. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/images/model-lifecycle.svg +0 -0
  130. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/images/navigation-primitives.svg +0 -0
  131. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/images/three-scales.svg +0 -0
  132. {hypertopos-0.2.2 → hypertopos-0.3.0}/docs/quickstart.md +0 -0
  133. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/builder/__init__.py +0 -0
  134. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/builder/_stats.py +0 -0
  135. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/builder/_writer.py +0 -0
  136. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/builder/derived.py +0 -0
  137. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/builder/mapping.py +0 -0
  138. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/cli/__init__.py +0 -0
  139. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/cli/__main__.py +0 -0
  140. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/cli/info.py +0 -0
  141. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/cli/main.py +0 -0
  142. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/cli/schema.py +0 -0
  143. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/cli/sources.py +0 -0
  144. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/engine/__init__.py +0 -0
  145. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/engine/calibration.py +0 -0
  146. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/engine/chains.py +0 -0
  147. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/engine/composition.py +0 -0
  148. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/engine/forecast.py +0 -0
  149. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/engine/geometry.py +0 -0
  150. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/engine/invalidation.py +0 -0
  151. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/engine/investigation.py +0 -0
  152. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/model/__init__.py +0 -0
  153. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/model/manifest.py +0 -0
  154. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/model/objects.py +0 -0
  155. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/model/sphere.py +0 -0
  156. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/navigation/__init__.py +0 -0
  157. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/sphere.py +0 -0
  158. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/storage/__init__.py +0 -0
  159. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/storage/_schemas.py +0 -0
  160. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/storage/cache.py +0 -0
  161. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/utils/__init__.py +0 -0
  162. {hypertopos-0.2.2 → hypertopos-0.3.0}/hypertopos/utils/arrow.py +0 -0
  163. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/.gitkeep +0 -0
  164. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/fixtures/generate_fixtures.py +0 -0
  165. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_arrow_utils.py +0 -0
  166. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_builder_alias.py +0 -0
  167. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_builder_edges.py +0 -0
  168. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_cache.py +0 -0
  169. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_chains.py +0 -0
  170. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_cli.py +0 -0
  171. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_cli_alias.py +0 -0
  172. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_cli_sources.py +0 -0
  173. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_compaction.py +0 -0
  174. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_composition.py +0 -0
  175. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_continuous_event_dims.py +0 -0
  176. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_data_quality_prop.py +0 -0
  177. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_detection.py +0 -0
  178. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_edge_table.py +0 -0
  179. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_engine.py +0 -0
  180. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_forecast.py +0 -0
  181. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_geometry_stats.py +0 -0
  182. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_hybrid_search.py +0 -0
  183. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_invalidation.py +0 -0
  184. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_investigation.py +0 -0
  185. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_mapping.py +0 -0
  186. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_model.py +0 -0
  187. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_mvcc_session.py +0 -0
  188. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_navigator_alerts.py +0 -0
  189. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_observability.py +0 -0
  190. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_precision_stack.py +0 -0
  191. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_reputation.py +0 -0
  192. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_row_id_cache.py +0 -0
  193. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_sphere.py +0 -0
  194. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_sphere_overview_perf.py +0 -0
  195. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_stats_guard.py +0 -0
  196. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_storage.py +0 -0
  197. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_streaming_reads.py +0 -0
  198. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_temporal_centroids.py +0 -0
  199. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_trajectory_ann.py +0 -0
  200. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_witness_cohort.py +0 -0
  201. {hypertopos-0.2.2 → hypertopos-0.3.0}/tests/test_witness_set.py +0 -0
@@ -5,6 +5,44 @@ 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.0] — 2026-04-12
9
+
10
+ > **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.
11
+ >
12
+ > **Mandatory rebuild for spheres built before this release.** See the Migration section below.
13
+
14
+ ### Changed
15
+
16
+ - `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.
17
+ - 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.
18
+ - 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.
19
+
20
+ ### Added
21
+
22
+ - `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.
23
+ - 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.
24
+
25
+ ### Fixed
26
+
27
+ - `_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.
28
+ - `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.
29
+ - 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`.
30
+
31
+ ### Security
32
+
33
+ - `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.
34
+
35
+ ### Removed
36
+
37
+ - `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`.
38
+ - `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.
39
+ - 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.
40
+
41
+ ### Migration
42
+
43
+ - **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.
44
+ - 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.
45
+
8
46
  ## [0.2.2] — 2026-04-11
9
47
 
10
48
  ### 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.0
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,13 @@
2
2
 
3
3
  > Planned direction. Priorities may change based on feedback.
4
4
 
5
- ## Current: 0.2.2
5
+ ## Current: 0.3.0
6
+
7
+ 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.
8
+
9
+ ---
10
+
11
+ ## 0.2.2
6
12
 
7
13
  As-of graph reconstruction across edge-table graph primitives + latency fix on `detect_cross_pattern_discrepancy`. See CHANGELOG.
8
14
 
@@ -26,33 +32,57 @@ First public release — full GDS stack, π1–π12, builder, MCP server, valida
26
32
 
27
33
  ---
28
34
 
29
- ## Plan: 0.3.0
35
+ ## Plan: 0.3.1
30
36
 
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)
37
+ Two production-quality improvements on top of the 0.3.0 storage layer:
35
38
 
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
39
+ - **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 α.
40
+ - **Diverse-coverage anomaly summarisation** — `select="diverse"` option using lazy-greedy submodular maximisation, plus a `representativeness` column ("this 1 anomaly stands in for 38 cases").
41
+ - **Temporal build optimization** — pre-aggregate event counts before the temporal loop, eliminating per-window event table rescans.
42
42
 
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.
43
+ ---
47
44
 
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
45
+ ## Plan: 0.3.2
46
+
47
+ README and docs narrative restructure.
48
+
49
+ ---
50
+
51
+ ## Plan: 0.4.0
52
+
53
+ TBD — picked after 0.3.2 lands.
50
54
 
51
55
  ---
52
56
 
53
57
  ## Future
54
58
 
59
+ **Detection quality**
60
+ - Edge-derived dimensions + temporal motif matcher
61
+ - Confidence scoring, robust estimators, multi-scale resolution — improve anomaly precision and reduce false positives on heavy-tail and multi-modal populations
62
+ - Lazy chain geometry — on-demand chain delta vectors via sampled population calibration; supplements the build-time `chain_lines` path
63
+
64
+ **Builder evolution**
65
+ - Incremental rebuild — geometry-only without `--force` wipe
66
+ - Generalized (d+m+g+t+s) dimension blocks — geographic / metric / semantic dimension support
67
+
68
+ **PassiveScanner evolution**
69
+ - Native temporal source support for direct temporal inputs, without requiring manual dataset plumbing in benchmark scripts
70
+ - Optional weighted scoring mode that uses continuous intensity instead of binary counts
71
+ - **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.
72
+
73
+ **Code refactoring**
74
+ - Break up oversized modules into smaller, domain-focused components
75
+ - Reduce coupling between core layers by replacing private cross-layer access with explicit interfaces where practical
76
+ - Tighten error handling in hot paths so failures are surfaced more consistently
77
+ - Consolidate repeated orchestration logic into shared helpers instead of duplicating it across modules
78
+ - Incremental refactor: preserve behavior, improve structure, then revisit deeper architectural boundaries
79
+
80
+ **Cross-sphere capabilities**
55
81
  - Cross-sphere comparison — dimensionless metrics across independently calibrated coordinate spaces
56
82
  - What-if analysis — hypothetical edge changes producing modified coordinate vectors
83
+
84
+ **Enterprise / governance**
57
85
  - Dimension access control — per-agent visibility constraints on delta dimensions
86
+
87
+ **Tooling**
58
88
  - 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
 
@@ -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
@@ -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"
@@ -1380,16 +1380,24 @@ class GDSBuilder:
1380
1380
  schema_names = set(line_reg.table.schema.names)
1381
1381
 
1382
1382
  ts_candidates = (
1383
- "timestamp", "ts", "event_time", "created_at", "tx_date", "date",
1383
+ "timestamp", "ts", "event_time", "tx_date", "date",
1384
1384
  )
1385
1385
  amt_candidates = (
1386
1386
  "amount_received", "amount", "amount_paid", "value",
1387
- "total", "amt",
1387
+ "total", "amt", "fare_amount", "total_amount",
1388
1388
  )
1389
1389
 
1390
1390
  ts_col = next(
1391
1391
  (c for c in ts_candidates if c in schema_names), None,
1392
1392
  )
1393
+ # Type-based fallback: first non-metadata column with timestamp type
1394
+ _META_COLS = {"created_at", "changed_at", "version"}
1395
+ if ts_col is None:
1396
+ import pyarrow as pa
1397
+ for field in line_reg.table.schema:
1398
+ if pa.types.is_timestamp(field.type) and field.name not in _META_COLS:
1399
+ ts_col = field.name
1400
+ break
1393
1401
  amt_col = next(
1394
1402
  (c for c in amt_candidates if c in schema_names), None,
1395
1403
  )
@@ -1417,23 +1425,31 @@ class GDSBuilder:
1417
1425
  to_arr = event_table[cfg.to_col]
1418
1426
  event_key_arr = event_table["primary_key"]
1419
1427
 
1420
- # Timestamp
1428
+ # Timestamp — resolve from config, then name heuristic, then type fallback
1421
1429
  ts_arr: pa.Array
1422
1430
  ts_col = cfg.timestamp_col
1423
1431
  if ts_col and ts_col in schema_names:
1424
1432
  ts_arr = self._to_epoch_seconds(event_table[ts_col])
1425
1433
  else:
1426
- # Try common column names
1427
1434
  for name in ("timestamp", "ts", "date", "created_at", "tx_date"):
1428
1435
  if name in schema_names:
1429
1436
  ts_arr = self._to_epoch_seconds(event_table[name])
1430
1437
  break
1431
1438
  else:
1432
- ts_arr = pa.array(
1433
- [0.0] * len(event_table), type=pa.float64(),
1434
- )
1439
+ # Type-based fallback: first non-metadata column with timestamp type
1440
+ _META = {"created_at", "changed_at", "version"}
1441
+ ts_resolved = False
1442
+ for field in event_table.schema:
1443
+ if pa.types.is_timestamp(field.type) and field.name not in _META:
1444
+ ts_arr = self._to_epoch_seconds(event_table[field.name])
1445
+ ts_resolved = True
1446
+ break
1447
+ if not ts_resolved:
1448
+ ts_arr = pa.array(
1449
+ [0.0] * len(event_table), type=pa.float64(),
1450
+ )
1435
1451
 
1436
- # Amount
1452
+ # Amount — resolve from config, then name heuristic
1437
1453
  amt_arr: pa.Array
1438
1454
  amt_col = cfg.amount_col
1439
1455
  if amt_col and amt_col in schema_names:
@@ -1441,8 +1457,10 @@ class GDSBuilder:
1441
1457
  pc.fill_null(event_table[amt_col], 0.0), pa.float64(),
1442
1458
  )
1443
1459
  else:
1444
- # Try common column names
1445
- for name in ("amount", "value", "total", "amt"):
1460
+ for name in (
1461
+ "amount", "value", "total", "amt",
1462
+ "fare_amount", "total_amount", "amount_received", "amount_paid",
1463
+ ):
1446
1464
  if name in schema_names:
1447
1465
  amt_arr = pc.cast(
1448
1466
  pc.fill_null(event_table[name], 0.0), pa.float64(),
@@ -2222,6 +2240,12 @@ class GDSBuilder:
2222
2240
  _, stats = _build_and_write(pat_id, pat)
2223
2241
  pattern_stats[pat_id] = stats
2224
2242
 
2243
+ # 2.5 Precompute per-entity contagion stats for every pattern with an
2244
+ # edge table. The runtime scanner reads this small table directly
2245
+ # instead of replaying the full edge table on every passive_scan /
2246
+ # composite_risk call.
2247
+ self._build_contagion_stats(_stats_writer)
2248
+
2225
2249
  # 3. Write sphere.json
2226
2250
  sphere_data = self._build_sphere_json(pattern_stats)
2227
2251
  meta_dir = self.output_path / "_gds_meta"
@@ -2241,6 +2265,106 @@ class GDSBuilder:
2241
2265
 
2242
2266
  return str(self.output_path)
2243
2267
 
2268
+ def _resolve_anchor_pattern_for_edge_table(
2269
+ self, pat_id: str, pat: _PatternReg,
2270
+ ) -> str | None:
2271
+ """Find which anchor pattern's geometry holds the is_anomaly bitmap
2272
+ for entities appearing in *pat*'s edge table.
2273
+
2274
+ For non-event patterns the pattern is its own anchor (its edges connect
2275
+ entities of its own line). For event patterns we walk the relations and
2276
+ return the anchor that owns the entity line of the first matching
2277
+ relation. Mirrors PassiveScanner._resolve_anchor_for_event so the
2278
+ precomputed table covers exactly the keys the scanner asks about at
2279
+ runtime.
2280
+ """
2281
+ if pat.pattern_type != "event":
2282
+ return pat_id
2283
+ for other_id, other in self._patterns.items():
2284
+ if other.pattern_type != "anchor" or other_id == pat_id:
2285
+ continue
2286
+ for rel in pat.relations:
2287
+ if rel.line_id == other.entity_line:
2288
+ return other_id
2289
+ return None
2290
+
2291
+ def _build_contagion_stats(self, stats_writer: Any) -> None:
2292
+ """Second pass: precompute per-entity contagion statistics for every
2293
+ pattern that has an edge table on disk. Writes one Lance table per
2294
+ pattern under ``_gds_meta/contagion_stats/{pattern_id}.lance`` with a
2295
+ BTREE index on ``primary_key``.
2296
+
2297
+ Schema: primary_key, neighbor_count (int32), anomalous_neighbor_count
2298
+ (int32), contagion_ratio (float32). Only entities that appear as
2299
+ at least one edge endpoint are included, so neighbor_count is
2300
+ always >= 1 in practice.
2301
+
2302
+ The runtime scanner reads this small table directly instead of replaying
2303
+ the full edge table on every passive_scan / composite_risk call.
2304
+ """
2305
+ import math
2306
+ from collections import defaultdict
2307
+
2308
+ import lance as _lance
2309
+
2310
+ for pat_id, pat in self._patterns.items():
2311
+ edge_path = self.output_path / "edges" / pat_id / "data.lance"
2312
+ if not edge_path.exists():
2313
+ continue
2314
+ anchor_pat_id = self._resolve_anchor_pattern_for_edge_table(pat_id, pat)
2315
+ if anchor_pat_id is None:
2316
+ continue
2317
+ edges_ds = _lance.dataset(str(edge_path))
2318
+ if edges_ds.count_rows() == 0:
2319
+ continue
2320
+ edges_table = edges_ds.scanner(columns=["from_key", "to_key"]).to_table()
2321
+ from_arr = edges_table["from_key"].to_pylist()
2322
+ to_arr = edges_table["to_key"].to_pylist()
2323
+ neighbors: dict[str, set[str]] = defaultdict(set)
2324
+ for f, t in zip(from_arr, to_arr, strict=False):
2325
+ if f != t:
2326
+ neighbors[f].add(t)
2327
+ neighbors[t].add(f)
2328
+ if not neighbors:
2329
+ continue
2330
+
2331
+ geom_path = (
2332
+ self.output_path / "geometry" / anchor_pat_id / "v=1" / "data.lance"
2333
+ )
2334
+ if not geom_path.exists():
2335
+ continue
2336
+ anomalous: set[str] = set()
2337
+ geom_table = _lance.dataset(str(geom_path)).scanner(
2338
+ columns=["primary_key", "is_anomaly"],
2339
+ ).to_table()
2340
+ pk_col = geom_table["primary_key"]
2341
+ anom_col = geom_table["is_anomaly"]
2342
+ for i in range(geom_table.num_rows):
2343
+ if anom_col[i].as_py():
2344
+ anomalous.add(pk_col[i].as_py())
2345
+
2346
+ primary_keys = sorted(neighbors.keys())
2347
+ nbr_counts: list[int] = []
2348
+ anom_counts: list[int] = []
2349
+ ratios: list[float] = []
2350
+ for k in primary_keys:
2351
+ nbs = neighbors[k]
2352
+ total = len(nbs)
2353
+ anom = len(nbs & anomalous)
2354
+ nbr_counts.append(total)
2355
+ anom_counts.append(anom)
2356
+ ratios.append(anom / total if total > 0 else math.nan)
2357
+
2358
+ contagion_table = pa.table(
2359
+ {
2360
+ "primary_key": pa.array(primary_keys, type=pa.string()),
2361
+ "neighbor_count": pa.array(nbr_counts, type=pa.int32()),
2362
+ "anomalous_neighbor_count": pa.array(anom_counts, type=pa.int32()),
2363
+ "contagion_ratio": pa.array(ratios, type=pa.float32()),
2364
+ },
2365
+ )
2366
+ stats_writer.write_contagion_stats(pat_id, contagion_table)
2367
+
2244
2368
  def incremental_update(
2245
2369
  self,
2246
2370
  pattern_id: str,
@@ -3706,3 +3830,22 @@ class GDSBuilder:
3706
3830
  f"Pattern '{pat_id}' event dimension: "
3707
3831
  f"column '{edim.column}' must be numeric, got '{col_type}'"
3708
3832
  )
3833
+
3834
+ # A pattern with zero declared dimensions has no geometry to compute —
3835
+ # delta vectors collapse to width 0. Reject up front: silent acceptance
3836
+ # used to be possible under Lance format 2.0, but format 2.1+ panics on
3837
+ # fixed_size_list[0] columns (lance-format/lance#5102), and an anomaly
3838
+ # geometry without dimensions is meaningless regardless of the storage
3839
+ # format. Tracked properties count as dimension sources because they
3840
+ # become prop_columns in the resolved delta width.
3841
+ if (
3842
+ not pat.relations
3843
+ and not pat.event_dimensions
3844
+ and not pat.tracked_properties
3845
+ ):
3846
+ raise ValueError(
3847
+ f"Pattern '{pat_id}' has no dimensions: declare at least one "
3848
+ f"relation, event dimension, derived dimension, or "
3849
+ f"tracked_properties entry. A pattern with zero dimensions "
3850
+ f"cannot produce a meaningful geometry."
3851
+ )
@@ -750,6 +750,14 @@ def _add_chain_line(
750
750
  ts_col = col_name
751
751
  needed_cols.append(ts_col)
752
752
  break
753
+ # Type-based fallback: first column with timestamp type
754
+ if ts_col is None:
755
+ import pyarrow as pa
756
+ for field in event_table.schema:
757
+ if pa.types.is_timestamp(field.type):
758
+ ts_col = field.name
759
+ needed_cols.append(ts_col)
760
+ break
753
761
 
754
762
  cat_col = None
755
763
  for col in ("receiving_currency", "category", "k_symbol", "type"):
@@ -759,7 +767,7 @@ def _add_chain_line(
759
767
  break
760
768
 
761
769
  amt_col = None
762
- for col in ("amount_received", "amount", "extendedprice"):
770
+ for col in ("amount_received", "amount", "extendedprice", "fare_amount", "total_amount"):
763
771
  if col in event_table.schema.names:
764
772
  amt_col = col
765
773
  needed_cols.append(amt_col)