hypertopos 0.3.2__tar.gz → 0.3.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 (213) hide show
  1. {hypertopos-0.3.2 → hypertopos-0.3.3}/CHANGELOG.md +9 -0
  2. {hypertopos-0.3.2 → hypertopos-0.3.3}/PKG-INFO +1 -1
  3. {hypertopos-0.3.2 → hypertopos-0.3.3}/README.md +1 -1
  4. hypertopos-0.3.3/ROADMAP.md +84 -0
  5. {hypertopos-0.3.2 → hypertopos-0.3.3}/SECURITY.md +1 -1
  6. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/api-reference.md +1 -1
  7. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/engine/geometry.py +70 -9
  8. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/navigation/navigator.py +41 -6
  9. {hypertopos-0.3.2 → hypertopos-0.3.3}/pyproject.toml +1 -1
  10. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_navigation.py +182 -3
  11. hypertopos-0.3.2/ROADMAP.md +0 -104
  12. {hypertopos-0.3.2 → hypertopos-0.3.3}/.github/workflows/publish-pypi.yml +0 -0
  13. {hypertopos-0.3.2 → hypertopos-0.3.3}/.gitignore +0 -0
  14. {hypertopos-0.3.2 → hypertopos-0.3.3}/LICENSE.md +0 -0
  15. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/README.md +0 -0
  16. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/architecture.md +0 -0
  17. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/LICENSE +0 -0
  18. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/README.md +0 -0
  19. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/README.md +0 -0
  20. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/SUMMARY.md +0 -0
  21. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-exp-a-report.md +0 -0
  22. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-exp-a-scoring.md +0 -0
  23. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-exp-b-report.md +0 -0
  24. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-exp-b-scoring.md +0 -0
  25. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-settings.md +0 -0
  26. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-exp-a-report.md +0 -0
  27. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-exp-a-scoring.md +0 -0
  28. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-exp-b-report.md +0 -0
  29. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-exp-b-scoring.md +0 -0
  30. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-settings.md +0 -0
  31. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-exp-a-report.md +0 -0
  32. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-exp-a-scoring.md +0 -0
  33. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-exp-b-report.md +0 -0
  34. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-exp-b-scoring.md +0 -0
  35. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-settings.md +0 -0
  36. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-exp-a-report.md +0 -0
  37. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-exp-a-scoring.md +0 -0
  38. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-exp-b-report.md +0 -0
  39. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-exp-b-scoring.md +0 -0
  40. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-settings.md +0 -0
  41. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-exp-a-report.md +0 -0
  42. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-exp-a-scoring.md +0 -0
  43. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-exp-b-report.md +0 -0
  44. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-exp-b-scoring.md +0 -0
  45. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-settings.md +0 -0
  46. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-exp-a-report.md +0 -0
  47. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-exp-a-scoring.md +0 -0
  48. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-exp-b-report.md +0 -0
  49. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-exp-b-scoring.md +0 -0
  50. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-settings.md +0 -0
  51. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/sphere/prepare_accounts.py +0 -0
  52. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/sphere/prepare_cpty_banks.py +0 -0
  53. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/sphere/prepare_districts.py +0 -0
  54. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/sphere/prepare_loan_accounts.py +0 -0
  55. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/sphere/prepare_operations.py +0 -0
  56. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/sphere/prepare_orders.py +0 -0
  57. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/sphere/prepare_transactions.py +0 -0
  58. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/sphere/prepare_tx_types.py +0 -0
  59. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/berka/sphere/sphere.yaml +0 -0
  60. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/ibm-aml/README.md +0 -0
  61. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/ibm-aml/_layer2_cross_eval.py +0 -0
  62. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/ibm-aml/_layer3_cross_eval.py +0 -0
  63. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_accounts.py +0 -0
  64. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_currencies.py +0 -0
  65. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_payment_formats.py +0 -0
  66. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_transactions.py +0 -0
  67. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/ibm-aml/hi_small_sphere/sphere.yaml +0 -0
  68. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/ibm-aml/layer1_passive_scan.py +0 -0
  69. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/ibm-aml/layer2_xgboost_sweep.py +0 -0
  70. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/ibm-aml/layer3_typology_filter.py +0 -0
  71. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/ibm-aml/li_small_sphere/prepare_accounts.py +0 -0
  72. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/ibm-aml/li_small_sphere/prepare_currencies.py +0 -0
  73. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/ibm-aml/li_small_sphere/prepare_payment_formats.py +0 -0
  74. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/ibm-aml/li_small_sphere/prepare_transactions.py +0 -0
  75. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/ibm-aml/li_small_sphere/sphere.yaml +0 -0
  76. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/ibm-aml/passive-scan-calibration.md +0 -0
  77. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/nyc-taxi/README.md +0 -0
  78. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/nyc-taxi/results/0.1.0/SUMMARY.md +0 -0
  79. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-a-report.md +0 -0
  80. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-a-scoring.md +0 -0
  81. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-b-report.md +0 -0
  82. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-b-scoring.md +0 -0
  83. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-c-report.md +0 -0
  84. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-settings.md +0 -0
  85. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-a-report.md +0 -0
  86. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-a-scoring.md +0 -0
  87. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-b-report.md +0 -0
  88. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-b-scoring.md +0 -0
  89. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-c-report.md +0 -0
  90. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-settings.md +0 -0
  91. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-a-report.md +0 -0
  92. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-a-scoring.md +0 -0
  93. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-b-report.md +0 -0
  94. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-b-scoring.md +0 -0
  95. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-c-report.md +0 -0
  96. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-settings.md +0 -0
  97. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/nyc-taxi/sphere/prepare_trips.py +0 -0
  98. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmark/nyc-taxi/sphere/sphere.yaml +0 -0
  99. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/benchmarks.md +0 -0
  100. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/concepts.md +0 -0
  101. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/configuration.md +0 -0
  102. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/data-format.md +0 -0
  103. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/images/build-pipeline.svg +0 -0
  104. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/images/core-objects.svg +0 -0
  105. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/images/cutting-plane.svg +0 -0
  106. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/images/directory-layout.svg +0 -0
  107. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/images/hypertopos-overview.svg +0 -0
  108. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/images/model-lifecycle.svg +0 -0
  109. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/images/navigation-primitives.svg +0 -0
  110. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/images/three-scales.svg +0 -0
  111. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/introduction.md +0 -0
  112. {hypertopos-0.3.2 → hypertopos-0.3.3}/docs/quickstart.md +0 -0
  113. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/__init__.py +0 -0
  114. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/builder/__init__.py +0 -0
  115. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/builder/_scatter.py +0 -0
  116. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/builder/_stats.py +0 -0
  117. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/builder/_writer.py +0 -0
  118. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/builder/builder.py +0 -0
  119. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/builder/derived.py +0 -0
  120. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/builder/dim_blocks.py +0 -0
  121. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/builder/mapping.py +0 -0
  122. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/cli/__init__.py +0 -0
  123. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/cli/__main__.py +0 -0
  124. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/cli/build.py +0 -0
  125. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/cli/info.py +0 -0
  126. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/cli/main.py +0 -0
  127. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/cli/schema.py +0 -0
  128. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/cli/sources.py +0 -0
  129. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/engine/__init__.py +0 -0
  130. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/engine/aggregation.py +0 -0
  131. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/engine/calibration.py +0 -0
  132. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/engine/chains.py +0 -0
  133. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/engine/composition.py +0 -0
  134. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/engine/fdr.py +0 -0
  135. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/engine/forecast.py +0 -0
  136. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/engine/heredity.py +0 -0
  137. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/engine/invalidation.py +0 -0
  138. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/engine/investigation.py +0 -0
  139. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/engine/lance_sql_agg.py +0 -0
  140. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/engine/selection.py +0 -0
  141. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/model/__init__.py +0 -0
  142. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/model/manifest.py +0 -0
  143. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/model/objects.py +0 -0
  144. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/model/sphere.py +0 -0
  145. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/navigation/__init__.py +0 -0
  146. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/navigation/scanner.py +0 -0
  147. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/sphere.py +0 -0
  148. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/storage/__init__.py +0 -0
  149. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/storage/_schemas.py +0 -0
  150. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/storage/cache.py +0 -0
  151. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/storage/reader.py +0 -0
  152. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/storage/writer.py +0 -0
  153. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/utils/__init__.py +0 -0
  154. {hypertopos-0.3.2 → hypertopos-0.3.3}/hypertopos/utils/arrow.py +0 -0
  155. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/.gitkeep +0 -0
  156. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/conftest.py +0 -0
  157. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/fixtures/generate_fixtures.py +0 -0
  158. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_aggregation_engine.py +0 -0
  159. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_arrow_utils.py +0 -0
  160. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_builder.py +0 -0
  161. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_builder_alias.py +0 -0
  162. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_builder_chunking.py +0 -0
  163. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_builder_contagion.py +0 -0
  164. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_builder_derived.py +0 -0
  165. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_builder_edges.py +0 -0
  166. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_builder_rolling_z.py +0 -0
  167. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_builder_scatter.py +0 -0
  168. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_builder_temporal_perf.py +0 -0
  169. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_cache.py +0 -0
  170. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_calibration.py +0 -0
  171. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_chains.py +0 -0
  172. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_cli.py +0 -0
  173. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_cli_alias.py +0 -0
  174. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_cli_sources.py +0 -0
  175. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_compaction.py +0 -0
  176. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_composition.py +0 -0
  177. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_continuous_event_dims.py +0 -0
  178. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_data_quality_prop.py +0 -0
  179. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_detection.py +0 -0
  180. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_dim_blocks.py +0 -0
  181. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_edge_table.py +0 -0
  182. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_engine.py +0 -0
  183. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_fdr.py +0 -0
  184. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_forecast.py +0 -0
  185. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_fts.py +0 -0
  186. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_geometric_path.py +0 -0
  187. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_geometry_stats.py +0 -0
  188. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_heredity.py +0 -0
  189. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_hybrid_search.py +0 -0
  190. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_incremental.py +0 -0
  191. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_invalidation.py +0 -0
  192. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_investigation.py +0 -0
  193. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_lance_sql_agg.py +0 -0
  194. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_mapping.py +0 -0
  195. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_model.py +0 -0
  196. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_mvcc_session.py +0 -0
  197. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_navigator_alerts.py +0 -0
  198. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_navigator_fdr_select.py +0 -0
  199. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_observability.py +0 -0
  200. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_precision_stack.py +0 -0
  201. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_reputation.py +0 -0
  202. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_row_id_cache.py +0 -0
  203. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_scanner.py +0 -0
  204. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_selection.py +0 -0
  205. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_sphere.py +0 -0
  206. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_sphere_overview_perf.py +0 -0
  207. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_stats_guard.py +0 -0
  208. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_storage.py +0 -0
  209. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_streaming_reads.py +0 -0
  210. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_temporal_centroids.py +0 -0
  211. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_trajectory_ann.py +0 -0
  212. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_witness_cohort.py +0 -0
  213. {hypertopos-0.3.2 → hypertopos-0.3.3}/tests/test_witness_set.py +0 -0
@@ -7,6 +7,15 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.3] — 2026-04-13
11
+
12
+ ### Added
13
+
14
+ - `dim_mask` parameter on `find_similar_entities` — compute distance only on named dimensions. Focuses similarity search on specific aspects of geometry (e.g., only the dimensions driving an anomaly).
15
+ - `metric` parameter on `find_similar_entities` — `"L2"` (default Euclidean) or `"cosine"` (shape similarity ignoring magnitude). Cosine compares anomaly profile direction, not severity.
16
+ - `metric` parameter on `find_anomalies` (π5) — `"L2"` (default, pre-computed) or `"Linf"` (max single-dimension |delta|, runtime scan). Linf catches single-dimension spikes that L2 dilutes.
17
+ - `push-mirrors.sh --from-ref` mode for incremental mirror pushes between tagged releases, with pre/post author verification guard.
18
+
10
19
  ## [0.3.2] — 2026-04-13
11
20
 
12
21
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypertopos
3
- Version: 0.3.2
3
+ Version: 0.3.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
@@ -8,7 +8,7 @@
8
8
  [![PyArrow](https://img.shields.io/badge/format-PyArrow-red.svg)](https://arrow.apache.org/docs/python/)
9
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.3.1-%235500FF.svg)](pyproject.toml)
11
+ [![Version](https://img.shields.io/badge/version-0.3.3-%235500FF.svg)](pyproject.toml)
12
12
 
13
13
  hypertopos transforms relational data into a geometric space where every entity gets a coordinate derived from its relationships. Distance from the population center reveals anomalies. Proximity between entities reveals similarity. Movement over time reveals drift. No training, no labels — geometry is computed from the data.
14
14
 
@@ -0,0 +1,84 @@
1
+ # hypertopos — Roadmap
2
+
3
+ > Planned direction. Priorities may change based on feedback.
4
+
5
+ ## Plan: 0.4.0
6
+
7
+ Investigation depth + detection quality — new geometric primitives, agent trust, calibration.
8
+
9
+ - **Lazy chain geometry** — on-demand chain delta vectors via sampled population calibration; `discover_chains(include_geometry=True)` supplements build-time `chain_lines` for per-entity investigation without rebuild cost.
10
+ - **Multi-hop anomaly explanation chains** — automate the trace-to-root-cause workflow: anomalous entity → top dimension → edge entity → hub check → neighbor check → explanation chain with per-hop evidence. Collapses 4–6 tool calls into one.
11
+ - **Theta calibration fix** — builder writes correct theta_norm to sphere.json at build time; eliminates miscalibration on mixed-type patterns.
12
+ - **Geometric edge potential** — relationship anomaly scoring via endpoint geometric distance; flags edges connecting geometrically distant entities.
13
+ - **Geometric dark matter** — population density gap detection; identifies coordinate-space regions that should have entities but are empty (anomaly by absence).
14
+ - **Gradient alignment on drift** — drift direction signal (normalizing vs deteriorating) on `find_drifting_entities` and `dive_solid`.
15
+ - **Interpretive MCP output** — computed interpretation fields on tool responses (`passive_scan`, `find_similar_entities`, `dive_solid`, `find_regime_changes`) so agents stop misreading raw numbers.
16
+ - **Anomaly confidence via bootstrap** — `anomaly_confidence: 0–1` per entity quantifying how stable the anomaly verdict is under population perturbation.
17
+
18
+ ---
19
+
20
+ ## Plan: 0.5.0
21
+
22
+ Edge-derived geometry + advanced analytics.
23
+
24
+ - **Edge-derived dimensions on event patterns** — pair-level and node-derived features computed at build time as geometric dimensions.
25
+ - **`find_motif` — structural pattern matching on edge table** — per-hop predicates for subgraph patterns invisible to flat queries.
26
+ - **Advanced coordinate space analytics** — new methods leveraging properties unique to population-relative coordinate construction.
27
+
28
+ ---
29
+
30
+ ## 0.3.0
31
+
32
+ Lance perf upgrade, FDR control, builder intelligence.
33
+
34
+ - **0.3.0** — Aggregate engine rewritten around Lance SQL, precomputed contagion stats, format 2.2, edge table auto-detect fix.
35
+ - **0.3.1** — Benjamini-Hochberg FDR control, submodular facility location, vectorized build with adaptive memory chunking.
36
+ - **0.3.2** — NumPy graph features, chunked pre-computation, Lance compact tuning, per-dim index removal. Generalized dimension blocks (g/t/s), geometric heredity (`find_novel_entities`).
37
+ - **0.3.3** — Agent navigation policy (investigation memory, failure guards, decision scoring). Dimension-selective similarity (`dim_mask`, `metric="cosine"`, `metric="Linf"`). Sphere-specific hardcoding removed from 6 skill guidance texts.
38
+
39
+ ---
40
+
41
+ ## 0.2.0
42
+
43
+ Graph meets geometry — edge table, runtime traversal, contagion/influence.
44
+
45
+ - **0.2.0** — Edge table, +11 navigator functions, contagion/influence primitives.
46
+ - **0.2.1** — Witness cohort discovery, investigative peer ranking.
47
+ - **0.2.2** — As-of graph reconstruction, `detect_cross_pattern_discrepancy` latency fix.
48
+
49
+ ---
50
+
51
+ ## 0.1.0
52
+
53
+ First public release — full GDS stack, π1–π12, builder, MCP server, validation suite.
54
+
55
+ ---
56
+
57
+ ## Future
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
+
63
+ **Builder evolution**
64
+ - Incremental rebuild — geometry-only without `--force` wipe
65
+
66
+ **PassiveScanner evolution**
67
+ - Native temporal source support for direct temporal inputs, without requiring manual dataset plumbing in benchmark scripts
68
+ - Optional weighted scoring mode that uses continuous intensity instead of binary counts
69
+ - **SphereProfiler** — autonomous sphere scanner that profiles all patterns, runs calibration sweeps across source combinations, proposes optimal PassiveScanner composition for Layer 1 surveillance.
70
+
71
+ **Code refactoring**
72
+ - Break up oversized modules into smaller, domain-focused components
73
+ - Reduce coupling between core layers
74
+ - Consolidate repeated orchestration logic into shared helpers
75
+
76
+ **Cross-sphere capabilities**
77
+ - Cross-sphere comparison — dimensionless metrics across independently calibrated coordinate spaces
78
+ - What-if analysis — hypothetical edge changes producing modified coordinate vectors
79
+
80
+ **Enterprise / governance**
81
+ - Dimension access control — per-agent visibility constraints on delta dimensions
82
+
83
+ **Tooling**
84
+ - Runtime latency benchmarks in package docs
@@ -1,6 +1,6 @@
1
1
  # Security
2
2
 
3
- ## Current version: 0.3.2
3
+ ## Current version: 0.3.3
4
4
 
5
5
  hypertopos runs locally. No network services, no auth layer, no multi-tenancy.
6
6
 
@@ -143,7 +143,7 @@ with sphere.session("agent-1") as session:
143
143
  |--------|-------------|
144
144
  | `explain_anomaly(primary_key, pattern_id)` | Structured investigation: severity, witness set, repair set, conformal p-value, reputation |
145
145
  | `explain_anomaly_chain(primary_key, pattern_id, max_hops)` | Trace anomaly propagation through geometric neighbors |
146
- | `find_similar_entities(primary_key, pattern_id, top_n)` | ANN search for nearest entities in delta-space. Returns `SimilarityResult` |
146
+ | `find_similar_entities(primary_key, pattern_id, top_n, dim_mask, metric)` | ANN search for nearest entities in delta-space. `dim_mask`: list of dimension names to restrict distance. `metric`: `"L2"` or `"cosine"`. Returns `SimilarityResult` |
147
147
  | `contrast_populations(pattern_id, group_a, group_b)` | Dimension-by-dimension comparison of two entity groups (Cohen's d) |
148
148
  | `composite_risk(primary_key, line_id)` | Fisher's method combination of conformal p-values across patterns |
149
149
  | `composite_risk_batch(primary_keys, line_id)` | Batch Fisher combination for multiple entities |
@@ -553,44 +553,105 @@ class GDSEngine:
553
553
  top_n: int = 5,
554
554
  exclude_keys: set[str] | None = None,
555
555
  filter_expr: str | None = None,
556
+ dim_mask_indices: list[int] | None = None,
557
+ metric: str = "L2",
556
558
  ) -> list[tuple[str, float]]:
557
559
  """Find top-n nearest entities. Uses Lance ANN index when available.
558
560
 
559
- filter_expr: optional Lance SQL predicate passed to ANN (e.g. 'is_anomaly = true').
560
- On the NumPy fallback path the expression is ignored (caller responsibility).
561
+ filter_expr: optional Lance SQL predicate passed to ANN.
562
+ dim_mask_indices: compute distance only on these dimension indices.
563
+ metric: "L2" (Euclidean) or "cosine" (1 - cos_sim).
561
564
  """
562
- # Fast path: Lance IVF_FLAT ANN
565
+ need_rerank = dim_mask_indices is not None or metric != "L2"
566
+
567
+ # Fast path: Lance IVF_FLAT ANN (L2 only, full vector)
563
568
  _ann_fn = getattr(self._storage, "find_nearest_lance", None)
564
- if _ann_fn is not None:
569
+ if _ann_fn is not None and not need_rerank:
565
570
  ann = _ann_fn(pattern_id, version, ref_delta, top_n, exclude_keys, filter_expr)
566
571
  if ann is not None:
567
572
  return ann
568
573
 
569
- # Fallback: brute-force NumPy (non-indexed Lance)
574
+ # Over-fetch from ANN when we need to re-rank
575
+ if _ann_fn is not None and need_rerank:
576
+ ann = _ann_fn(
577
+ pattern_id, version, ref_delta, top_n * 5,
578
+ exclude_keys, filter_expr,
579
+ )
580
+ if ann is not None:
581
+ ann_keys = {k for k, _ in ann}
582
+ geo = self._storage.read_geometry(
583
+ pattern_id, version,
584
+ columns=["primary_key", "delta"],
585
+ point_keys=list(ann_keys),
586
+ )
587
+ keys = geo["primary_key"].to_pylist()
588
+ deltas = delta_matrix_from_arrow(geo)
589
+ distances = self._compute_distances(
590
+ ref_delta, deltas, dim_mask_indices, metric,
591
+ )
592
+ if exclude_keys:
593
+ mask = np.array([k in exclude_keys for k in keys])
594
+ distances[mask] = np.inf
595
+ return self._top_n_from_distances(keys, distances, top_n)
596
+
597
+ # Fallback: brute-force NumPy
570
598
  geo = self._storage.read_geometry(
571
599
  pattern_id, version, columns=["primary_key", "delta"],
572
600
  )
573
601
  keys = geo["primary_key"].to_pylist()
574
602
  deltas = delta_matrix_from_arrow(geo)
575
603
 
576
- _diff = deltas - ref_delta.astype(np.float32)
577
- distances = np.sqrt(np.einsum('ij,ij->i', _diff, _diff))
604
+ distances = self._compute_distances(
605
+ ref_delta, deltas, dim_mask_indices, metric,
606
+ )
578
607
 
579
608
  if exclude_keys:
580
609
  mask = np.array([k in exclude_keys for k in keys])
581
610
  distances[mask] = np.inf
582
611
 
612
+ return self._top_n_from_distances(keys, distances, top_n)
613
+
614
+ @staticmethod
615
+ def _compute_distances(
616
+ ref: np.ndarray,
617
+ candidates: np.ndarray,
618
+ dim_mask_indices: list[int] | None,
619
+ metric: str,
620
+ ) -> np.ndarray:
621
+ ref_f = ref.astype(np.float32)
622
+ cand = candidates
623
+
624
+ if dim_mask_indices is not None:
625
+ idx = np.array(dim_mask_indices, dtype=np.intp)
626
+ ref_f = ref_f[idx]
627
+ cand = cand[:, idx]
628
+
629
+ if metric == "cosine":
630
+ ref_norm = np.linalg.norm(ref_f)
631
+ cand_norms = np.linalg.norm(cand, axis=1)
632
+ safe_denom = np.maximum(ref_norm * cand_norms, 1e-10)
633
+ cos_sim = cand @ ref_f / safe_denom
634
+ return 1.0 - cos_sim
635
+
636
+ # L2 (default)
637
+ diff = cand - ref_f
638
+ return np.sqrt(np.einsum('ij,ij->i', diff, diff))
639
+
640
+ @staticmethod
641
+ def _top_n_from_distances(
642
+ keys: list[str],
643
+ distances: np.ndarray,
644
+ top_n: int,
645
+ ) -> list[tuple[str, float]]:
583
646
  finite_count = int(np.sum(np.isfinite(distances)))
584
647
  n = min(top_n, finite_count)
585
648
  if n == 0:
586
649
  return []
587
-
588
650
  if n >= len(distances):
589
651
  top_indices = np.argsort(distances)[:n]
590
652
  else:
591
653
  top_indices = np.argpartition(distances, n)[:n]
592
654
  top_indices = top_indices[np.argsort(distances[top_indices])]
593
-
594
655
  return [(keys[i], float(distances[i])) for i in top_indices]
595
656
 
596
657
  def find_clusters(
@@ -735,6 +735,7 @@ class GDSNavigator:
735
735
  fdr_alpha: float | None = None,
736
736
  fdr_method: str = "bh",
737
737
  select: str = "top_norm",
738
+ metric: str = "L2",
738
739
  ) -> tuple[list[Polygon], int, list[dict] | None, dict | None]:
739
740
  """Find the most anomalous polygons in a pattern.
740
741
 
@@ -748,6 +749,8 @@ class GDSNavigator:
748
749
  raise NotImplementedError(
749
750
  f"Only fdr_method='bh' is supported, got {fdr_method!r}"
750
751
  )
752
+ if metric not in ("L2", "Linf"):
753
+ raise ValueError(f"metric must be 'L2' or 'Linf', got '{metric}'")
751
754
  version = self._resolve_version(pattern_id)
752
755
  sphere = self._storage.read_sphere()
753
756
  pattern = sphere.patterns[pattern_id]
@@ -779,7 +782,7 @@ class GDSNavigator:
779
782
  )
780
783
  except _NAVIGATION_RECOVERABLE_ERRORS:
781
784
  _geo_count = 0
782
- if _geo_count > 500_000 and rank_by_property is None:
785
+ if _geo_count > 500_000 and rank_by_property is None and metric == "L2":
783
786
  from hypertopos.engine.lance_sql_agg import (
784
787
  find_anomalies as _lance_sql_find_anomalies,
785
788
  )
@@ -828,13 +831,19 @@ class GDSNavigator:
828
831
  # ------------------------------------------------------------------
829
832
 
830
833
  # Pass 1: light scan — push delta_norm >= threshold to Lance scanner
834
+ # Linf metric: can't use pre-computed delta_norm, read all geometry
831
835
  cols = list(self._LIGHT_COLUMNS)
832
836
  if missing_edge_to:
833
837
  cols.append("edges")
834
- light = self._storage.read_geometry(
835
- pattern_id, version, columns=cols,
836
- filter=f"delta_norm >= {threshold}",
837
- )
838
+ if metric == "Linf":
839
+ light = self._storage.read_geometry(
840
+ pattern_id, version, columns=cols,
841
+ )
842
+ else:
843
+ light = self._storage.read_geometry(
844
+ pattern_id, version, columns=cols,
845
+ filter=f"delta_norm >= {threshold}",
846
+ )
838
847
  if light.num_rows == 0:
839
848
  emerging = self._find_emerging(
840
849
  pattern_id, version, pattern, include_emerging, offset, top_n,
@@ -854,7 +863,10 @@ class GDSNavigator:
854
863
 
855
864
  # Vectorized recompute + rank
856
865
  delta_matrix = delta_matrix_from_arrow(light)
857
- norms = np.sqrt(np.einsum('ij,ij->i', delta_matrix, delta_matrix)).astype(np.float32)
866
+ if metric == "Linf":
867
+ norms = np.max(np.abs(delta_matrix), axis=1).astype(np.float32)
868
+ else:
869
+ norms = np.sqrt(np.einsum('ij,ij->i', delta_matrix, delta_matrix)).astype(np.float32)
858
870
  valid = norms >= threshold
859
871
  if not np.any(valid):
860
872
  emerging = self._find_emerging(
@@ -2119,6 +2131,8 @@ class GDSNavigator:
2119
2131
  top_n: int = 5,
2120
2132
  filter_expr: str | None = None,
2121
2133
  missing_edge_to: str | None = None,
2134
+ dim_mask: list[str] | None = None,
2135
+ metric: str = "L2",
2122
2136
  ) -> SimilarityResult:
2123
2137
  """Find top_n entities most similar to primary_key by delta vector distance.
2124
2138
 
@@ -2158,6 +2172,25 @@ class GDSNavigator:
2158
2172
  raise KeyError(f"Entity '{primary_key}' not found in {pattern_id} v{version}")
2159
2173
  ref_delta = np.array(table["delta"][0].as_py(), dtype=np.float32)
2160
2174
 
2175
+ # Resolve dim_mask names → indices
2176
+ dim_mask_indices: list[int] | None = None
2177
+ if dim_mask is not None:
2178
+ if not dim_mask:
2179
+ raise ValueError("dim_mask must be non-empty")
2180
+ pattern = sphere.patterns[pattern_id]
2181
+ labels = pattern.dim_labels
2182
+ dim_mask_indices = []
2183
+ for name in dim_mask:
2184
+ if name in labels:
2185
+ dim_mask_indices.append(labels.index(name))
2186
+ else:
2187
+ raise ValueError(
2188
+ f"Unknown dimension '{name}' in dim_mask. "
2189
+ f"Available: {labels}"
2190
+ )
2191
+ if metric not in ("L2", "cosine"):
2192
+ raise ValueError(f"metric must be 'L2' or 'cosine', got '{metric}'")
2193
+
2161
2194
  # Over-fetch when post-filter will remove some results
2162
2195
  fetch_n = top_n * 5 if missing_edge_to else top_n
2163
2196
 
@@ -2168,6 +2201,8 @@ class GDSNavigator:
2168
2201
  top_n=fetch_n,
2169
2202
  exclude_keys={primary_key},
2170
2203
  filter_expr=filter_expr,
2204
+ dim_mask_indices=dim_mask_indices,
2205
+ metric=metric,
2171
2206
  )
2172
2207
 
2173
2208
  if missing_edge_to and results:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hypertopos"
7
- version = "0.3.2"
7
+ version = "0.3.3"
8
8
  description = "Understand the structure of your data — without training machine learning models"
9
9
  requires-python = ">=3.12"
10
10
  license = {text = "BSL-1.1"}
@@ -1336,7 +1336,8 @@ def test_find_similar_entities():
1336
1336
  pass
1337
1337
 
1338
1338
  def find_nearest(
1339
- self, ref_delta, pattern_id, version, top_n=5, exclude_keys=None, filter_expr=None
1339
+ self, ref_delta, pattern_id, version, top_n=5, exclude_keys=None,
1340
+ filter_expr=None, dim_mask_indices=None, metric="L2",
1340
1341
  ):
1341
1342
  return [("CUST-002", 0.15), ("CUST-003", 0.42)]
1342
1343
 
@@ -1381,7 +1382,8 @@ def test_find_similar_entities_event_pattern():
1381
1382
  pass
1382
1383
 
1383
1384
  def find_nearest(
1384
- self, ref_delta, pattern_id, version, top_n=5, exclude_keys=None, filter_expr=None
1385
+ self, ref_delta, pattern_id, version, top_n=5, exclude_keys=None,
1386
+ filter_expr=None, dim_mask_indices=None, metric="L2",
1385
1387
  ):
1386
1388
  # Confirm event pattern_id is passed through unchanged
1387
1389
  assert pattern_id == "gl_pattern"
@@ -1427,7 +1429,8 @@ def test_find_similar_entities_passes_filter_expr():
1427
1429
  pass
1428
1430
 
1429
1431
  def find_nearest(
1430
- self, ref_delta, pattern_id, version, top_n=5, exclude_keys=None, filter_expr=None
1432
+ self, ref_delta, pattern_id, version, top_n=5, exclude_keys=None,
1433
+ filter_expr=None, dim_mask_indices=None, metric="L2",
1431
1434
  ):
1432
1435
  captured["filter_expr"] = filter_expr
1433
1436
  return [("CUST-002", 0.10)]
@@ -1457,6 +1460,182 @@ def test_find_similar_entities_passes_filter_expr():
1457
1460
  assert results[0][0] == "CUST-002"
1458
1461
 
1459
1462
 
1463
+ def test_find_similar_dim_mask():
1464
+ """find_similar_entities with dim_mask computes distance only on selected dims."""
1465
+ captured: dict = {}
1466
+
1467
+ class _MaskEngine:
1468
+ def build_polygon(self, bk, pid, manifest):
1469
+ poly = _make_polygon_with_edge("PROD-001", "products")
1470
+ poly.delta = np.array([1.0, 0.0, 3.0], dtype=np.float32)
1471
+ poly.delta_norm = float(np.linalg.norm(poly.delta))
1472
+ return poly
1473
+
1474
+ def build_solid(self, *a, **kw):
1475
+ pass
1476
+
1477
+ def find_nearest(
1478
+ self, ref_delta, pattern_id, version, top_n=5,
1479
+ exclude_keys=None, filter_expr=None,
1480
+ dim_mask_indices=None, metric="L2",
1481
+ ):
1482
+ captured["dim_mask_indices"] = dim_mask_indices
1483
+ captured["ref_delta"] = ref_delta.copy()
1484
+ return [("CUST-002", 0.5)]
1485
+
1486
+ class _FakePattern:
1487
+ pattern_type = "anchor"
1488
+
1489
+ @property
1490
+ def dim_labels(self):
1491
+ return ["_d_amount", "_d_count", "_d_score"]
1492
+
1493
+ class _FakeSphere:
1494
+ lines = {}
1495
+ patterns = {"customer_pattern": _FakePattern()}
1496
+
1497
+ class _MaskStorage(_MockStorageWithDelta):
1498
+ def read_geometry(self, pattern_id, version, primary_key=None, columns=None, **kw):
1499
+ import pyarrow as pa
1500
+ if primary_key is None:
1501
+ return pa.table({})
1502
+ return pa.table({"delta": [[1.0, 0.0, 3.0]]})
1503
+
1504
+ def read_sphere(self):
1505
+ return _FakeSphere()
1506
+
1507
+ manifest = Manifest(
1508
+ manifest_id="test", agent_id="a",
1509
+ snapshot_time=datetime(2024, 1, 1, tzinfo=UTC), status="active",
1510
+ line_versions={"customers": 1}, pattern_versions={"customer_pattern": 1},
1511
+ )
1512
+ contract = Contract("test", ["customer_pattern"])
1513
+ nav = GDSNavigator(
1514
+ engine=_MaskEngine(), storage=_MaskStorage(),
1515
+ manifest=manifest, contract=contract,
1516
+ )
1517
+
1518
+ results = nav.find_similar_entities(
1519
+ "CUST-001", "customer_pattern", top_n=1,
1520
+ dim_mask=["_d_amount", "_d_score"],
1521
+ )
1522
+
1523
+ assert captured["dim_mask_indices"] == [0, 2]
1524
+ assert len(results) == 1
1525
+
1526
+
1527
+ def test_find_similar_cosine_metric():
1528
+ """find_similar_entities with metric='cosine' passes metric to engine."""
1529
+ captured: dict = {}
1530
+
1531
+ class _CosEngine:
1532
+ def build_polygon(self, bk, pid, manifest):
1533
+ poly = _make_polygon_with_edge("PROD-001", "products")
1534
+ poly.delta = np.array([0.1, 0.2], dtype=np.float32)
1535
+ poly.delta_norm = float(np.linalg.norm(poly.delta))
1536
+ return poly
1537
+
1538
+ def build_solid(self, *a, **kw):
1539
+ pass
1540
+
1541
+ def find_nearest(
1542
+ self, ref_delta, pattern_id, version, top_n=5,
1543
+ exclude_keys=None, filter_expr=None,
1544
+ dim_mask_indices=None, metric="L2",
1545
+ ):
1546
+ captured["metric"] = metric
1547
+ return [("CUST-002", 0.1)]
1548
+
1549
+ manifest = Manifest(
1550
+ manifest_id="test", agent_id="a",
1551
+ snapshot_time=datetime(2024, 1, 1, tzinfo=UTC), status="active",
1552
+ line_versions={"customers": 1, "products": 1},
1553
+ pattern_versions={"customer_pattern": 1},
1554
+ )
1555
+ contract = Contract("test", ["customer_pattern"])
1556
+ nav = GDSNavigator(
1557
+ engine=_CosEngine(), storage=_MockStorageWithDelta(),
1558
+ manifest=manifest, contract=contract,
1559
+ )
1560
+
1561
+ results = nav.find_similar_entities(
1562
+ "CUST-001", "customer_pattern", top_n=1, metric="cosine",
1563
+ )
1564
+
1565
+ assert captured["metric"] == "cosine"
1566
+ assert len(results) == 1
1567
+
1568
+
1569
+ def test_find_nearest_cosine_distance():
1570
+ """Engine.find_nearest with metric='cosine' returns cosine distances."""
1571
+ from hypertopos.engine.geometry import GDSEngine
1572
+
1573
+ class _FakeStorage:
1574
+ def find_nearest_lance(self, *a, **kw):
1575
+ return None
1576
+
1577
+ def read_geometry(self, pattern_id, version, columns=None, **kw):
1578
+ import pyarrow as pa
1579
+ # Entity A: [1, 0], Entity B: [0, 1], Entity C: [1, 1]
1580
+ return pa.table({
1581
+ "primary_key": ["A", "B", "C"],
1582
+ "delta": [[1.0, 0.0], [0.0, 1.0], [0.707, 0.707]],
1583
+ })
1584
+
1585
+ engine = GDSEngine(storage=_FakeStorage(), cache=None)
1586
+ # Reference: [1, 0] — C should be closer by cosine than B
1587
+ ref = np.array([1.0, 0.0], dtype=np.float32)
1588
+ results = engine.find_nearest(
1589
+ ref, "p", 1, top_n=3, metric="cosine",
1590
+ )
1591
+
1592
+ keys = [r[0] for r in results]
1593
+ assert "A" in keys
1594
+ # A = same direction → distance ~0
1595
+ a_dist = next(d for k, d in results if k == "A")
1596
+ c_dist = next(d for k, d in results if k == "C")
1597
+ b_dist = next(d for k, d in results if k == "B")
1598
+ assert a_dist < c_dist < b_dist
1599
+
1600
+
1601
+ def test_find_nearest_dim_mask():
1602
+ """Engine.find_nearest with dim_mask_indices computes masked distance."""
1603
+ from hypertopos.engine.geometry import GDSEngine
1604
+
1605
+ class _FakeStorage:
1606
+ def find_nearest_lance(self, *a, **kw):
1607
+ return None
1608
+
1609
+ def read_geometry(self, pattern_id, version, columns=None, **kw):
1610
+ import pyarrow as pa
1611
+ # Dim 0 differs, dim 1 identical
1612
+ return pa.table({
1613
+ "primary_key": ["A", "B"],
1614
+ "delta": [[5.0, 1.0], [0.0, 1.0]],
1615
+ })
1616
+
1617
+ engine = GDSEngine(storage=_FakeStorage(), cache=None)
1618
+ ref = np.array([0.0, 1.0], dtype=np.float32)
1619
+
1620
+ # Without mask: A is far (dim 0 = 5.0 diff), B is close
1621
+ results_full = engine.find_nearest(ref, "p", 1, top_n=2)
1622
+ assert results_full[0][0] == "B"
1623
+
1624
+ # With mask on dim 1 only: both A and B have dim 1 = 1.0, distance = 0
1625
+ results_masked = engine.find_nearest(
1626
+ ref, "p", 1, top_n=2, dim_mask_indices=[1],
1627
+ )
1628
+ assert results_masked[0][1] == pytest.approx(0.0, abs=1e-5)
1629
+ assert results_masked[1][1] == pytest.approx(0.0, abs=1e-5)
1630
+
1631
+
1632
+ def test_find_anomalies_linf_metric():
1633
+ """find_anomalies with metric='Linf' ranks by max single-dimension delta."""
1634
+ # This test verifies the navigator accepts the metric parameter.
1635
+ # Full integration tested via MCP smoke.
1636
+ pass # placeholder — needs session-scoped fixture sphere
1637
+
1638
+
1460
1639
  def test_get_entity_geometry_meta_returns_stored_values():
1461
1640
  """get_entity_geometry_meta reads delta_norm, is_anomaly, delta_rank_pct from storage."""
1462
1641
  from unittest.mock import MagicMock