transform-tree 0.0.6__tar.gz → 0.0.7__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 (159) hide show
  1. {transform_tree-0.0.6 → transform_tree-0.0.7}/Cargo.toml +9 -6
  2. {transform_tree-0.0.6 → transform_tree-0.0.7}/PKG-INFO +1 -1
  3. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_math/Cargo.toml +7 -0
  4. transform_tree-0.0.7/crates/tf_tree_math/src/helicoid_backend.rs +284 -0
  5. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_math/src/interp.rs +15 -0
  6. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_math/src/lib.rs +2 -0
  7. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_math/src/quat.rs +61 -0
  8. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_math/tests/slerp_public.rs +30 -8
  9. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_py/Cargo.lock +29 -8
  10. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_py/Cargo.toml +4 -4
  11. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_py/src/tree.rs +2 -2
  12. {transform_tree-0.0.6 → transform_tree-0.0.7}/pyproject.toml +1 -1
  13. {transform_tree-0.0.6 → transform_tree-0.0.7}/LICENSE-APACHE +0 -0
  14. {transform_tree-0.0.6 → transform_tree-0.0.7}/LICENSE-MIT +0 -0
  15. {transform_tree-0.0.6 → transform_tree-0.0.7}/NOTICE +0 -0
  16. {transform_tree-0.0.6 → transform_tree-0.0.7}/README.md +0 -0
  17. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/Cargo.toml +0 -0
  18. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/LICENSE-APACHE +0 -0
  19. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/LICENSE-MIT +0 -0
  20. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/NOTICE +0 -0
  21. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/README.md +0 -0
  22. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/examples/control_loop.rs +0 -0
  23. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/examples/gen_domain_fixture.rs +0 -0
  24. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/examples/two_processes.rs +0 -0
  25. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/src/bin/rendezvous_child.rs +0 -0
  26. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/src/cache.rs +0 -0
  27. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/src/cbor.rs +0 -0
  28. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/src/frozen.rs +0 -0
  29. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/src/lib.rs +0 -0
  30. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/src/open.rs +0 -0
  31. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/src/tree.rs +0 -0
  32. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/src/unstable.rs +0 -0
  33. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/tests/await_frames.rs +0 -0
  34. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/tests/batch.rs +0 -0
  35. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/tests/batch_phases.rs +0 -0
  36. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/tests/behavior.rs +0 -0
  37. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/tests/clock_offset.rs +0 -0
  38. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/tests/common/mod.rs +0 -0
  39. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/tests/construction.rs +0 -0
  40. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/tests/counters.rs +0 -0
  41. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/tests/derivatives.rs +0 -0
  42. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/tests/enumeration.rs +0 -0
  43. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/tests/error_payloads.rs +0 -0
  44. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/tests/feature_gates.rs +0 -0
  45. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/tests/frozen.rs +0 -0
  46. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/tests/lookup.rs +0 -0
  47. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/tests/math_reexports.rs +0 -0
  48. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/tests/owned_writer.rs +0 -0
  49. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/tests/plan_cache_identity.rs +0 -0
  50. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/tests/rendezvous.rs +0 -0
  51. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/tests/tsan.rs +0 -0
  52. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree/tests/wide_stamps.rs +0 -0
  53. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_arena/Cargo.toml +0 -0
  54. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_arena/LICENSE-APACHE +0 -0
  55. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_arena/LICENSE-MIT +0 -0
  56. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_arena/NOTICE +0 -0
  57. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_arena/README.md +0 -0
  58. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_arena/src/check.rs +0 -0
  59. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_arena/src/frozen.rs +0 -0
  60. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_arena/src/header.rs +0 -0
  61. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_arena/src/heap.rs +0 -0
  62. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_arena/src/layout.rs +0 -0
  63. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_arena/src/lib.rs +0 -0
  64. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_arena/src/mapped.rs +0 -0
  65. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_arena/src/render_test.rs +0 -0
  66. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_arena/tests/heap_alignment.rs +0 -0
  67. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_bridge/Cargo.toml +0 -0
  68. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_bridge/examples/offer_cost.rs +0 -0
  69. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_bridge/src/authority.rs +0 -0
  70. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_bridge/src/clock.rs +0 -0
  71. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_bridge/src/config.rs +0 -0
  72. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_bridge/src/discover.rs +0 -0
  73. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_bridge/src/edgeindex.rs +0 -0
  74. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_bridge/src/edgemap.rs +0 -0
  75. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_bridge/src/ingest.rs +0 -0
  76. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_bridge/src/interner.rs +0 -0
  77. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_bridge/src/lib.rs +0 -0
  78. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_bridge/src/names.rs +0 -0
  79. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_bridge/src/statics.rs +0 -0
  80. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_bridge/src/stats.rs +0 -0
  81. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_bridge/tests/steady_state_alloc.rs +0 -0
  82. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_core/Cargo.toml +0 -0
  83. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_core/LICENSE-APACHE +0 -0
  84. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_core/LICENSE-MIT +0 -0
  85. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_core/NOTICE +0 -0
  86. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_core/README.md +0 -0
  87. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_core/src/arena_view.rs +0 -0
  88. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_core/src/bench_probe.rs +0 -0
  89. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_core/src/buffer.rs +0 -0
  90. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_core/src/counters.rs +0 -0
  91. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_core/src/crash.rs +0 -0
  92. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_core/src/crash_tests.rs +0 -0
  93. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_core/src/edge.rs +0 -0
  94. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_core/src/error.rs +0 -0
  95. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_core/src/frame.rs +0 -0
  96. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_core/src/layout.rs +0 -0
  97. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_core/src/lib.rs +0 -0
  98. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_core/src/loom_tests.rs +0 -0
  99. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_core/src/participant.rs +0 -0
  100. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_core/src/plan.rs +0 -0
  101. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_core/src/sample.rs +0 -0
  102. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_core/src/sync.rs +0 -0
  103. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_core/src/tests.rs +0 -0
  104. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_core/src/topology.rs +0 -0
  105. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ingest/Cargo.toml +0 -0
  106. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ingest/examples/gen_zstd_conformance.rs +0 -0
  107. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ingest/src/cdr.rs +0 -0
  108. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ingest/src/decompress.rs +0 -0
  109. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ingest/src/fixture.rs +0 -0
  110. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ingest/src/ingest.rs +0 -0
  111. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ingest/src/lib.rs +0 -0
  112. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ingest/src/report.rs +0 -0
  113. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ingest/src/source.rs +0 -0
  114. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ingest/src/spill.rs +0 -0
  115. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ingest/src/tft.rs +0 -0
  116. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ingest/testdata/ATTRIBUTION.md +0 -0
  117. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ingest/testdata/zstd_conformance.mcap +0 -0
  118. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ingest/tests/anomaly_corpus.rs +0 -0
  119. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ingest/tests/codec_free.rs +0 -0
  120. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ingest/tests/frozen_bag.rs +0 -0
  121. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ingest/tests/ingest.rs +0 -0
  122. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ingest/tests/memory.rs +0 -0
  123. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ingest/tests/record_ceiling.rs +0 -0
  124. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ipc/Cargo.toml +0 -0
  125. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ipc/LICENSE-APACHE +0 -0
  126. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ipc/LICENSE-MIT +0 -0
  127. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ipc/NOTICE +0 -0
  128. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ipc/README.md +0 -0
  129. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ipc/src/bin/ipc_child.rs +0 -0
  130. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ipc/src/client.rs +0 -0
  131. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ipc/src/error.rs +0 -0
  132. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ipc/src/fork.rs +0 -0
  133. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ipc/src/identity.rs +0 -0
  134. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ipc/src/lib.rs +0 -0
  135. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ipc/src/lockfile.rs +0 -0
  136. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ipc/src/ofd.rs +0 -0
  137. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ipc/src/open.rs +0 -0
  138. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ipc/src/procstat.rs +0 -0
  139. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ipc/src/rendezvous.rs +0 -0
  140. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ipc/src/runtime_dir.rs +0 -0
  141. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ipc/src/server.rs +0 -0
  142. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ipc/src/wire.rs +0 -0
  143. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_ipc/tests/multiprocess.rs +0 -0
  144. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_math/LICENSE-APACHE +0 -0
  145. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_math/LICENSE-MIT +0 -0
  146. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_math/NOTICE +0 -0
  147. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_math/README.md +0 -0
  148. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_math/src/dualquat.rs +0 -0
  149. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_math/src/iso3.rs +0 -0
  150. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_math/src/reference.rs +0 -0
  151. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_math/src/twist.rs +0 -0
  152. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_math/tests/proptests.rs +0 -0
  153. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_py/src/errors.rs +0 -0
  154. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_py/src/ingest.rs +0 -0
  155. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_py/src/lib.rs +0 -0
  156. {transform_tree-0.0.6 → transform_tree-0.0.7}/crates/tf_tree_py/src/offline.rs +0 -0
  157. {transform_tree-0.0.6 → transform_tree-0.0.7}/python/tf_tree/__init__.py +0 -0
  158. {transform_tree-0.0.6 → transform_tree-0.0.7}/python/tf_tree/_core.pyi +0 -0
  159. {transform_tree-0.0.6 → transform_tree-0.0.7}/python/tf_tree/py.typed +0 -0
@@ -20,7 +20,7 @@ exclude = [
20
20
 
21
21
  [workspace.package]
22
22
  # 0.0.x: every release may break every other (`SUPPORT.md`).
23
- version = "0.0.6"
23
+ version = "0.0.7"
24
24
  edition = "2021"
25
25
  # 1.87 is forced by `ruzstd 0.9.0` (`tf_tree_ingest`). The `msrv` job reads this
26
26
  # field; policy in `SUPPORT.md`.
@@ -57,16 +57,16 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(loom)'] }
57
57
 
58
58
  [workspace.dependencies]
59
59
  # Intra-workspace crates.
60
- tf_tree_math = { path = "crates/tf_tree_math", version = "0.0.6" }
61
- tf_tree_arena = { path = "crates/tf_tree_arena", version = "0.0.6" }
60
+ tf_tree_math = { path = "crates/tf_tree_math", version = "0.0.7" }
61
+ tf_tree_arena = { path = "crates/tf_tree_arena", version = "0.0.7" }
62
62
  # `shm` (`docs/PHASE2.md` §2): `linux_raw` backend, no C build; used only by `tf_tree_ipc`.
63
63
  libc = "0.2"
64
64
  rustix = { version = "1.1", default-features = false, features = ["mm", "fs", "std"] }
65
65
  # `default-features = false` is load-bearing: without it `--no-default-features`
66
66
  # downstream still enables `counters` (`docs/PHASE5.md` §5.5).
67
- tf_tree_core = { path = "crates/tf_tree_core", version = "0.0.6", default-features = false }
68
- tf_tree_ipc = { path = "crates/tf_tree_ipc", version = "0.0.6" }
69
- tf_tree = { path = "crates/tf_tree", version = "0.0.6", default-features = false }
67
+ tf_tree_core = { path = "crates/tf_tree_core", version = "0.0.7", default-features = false }
68
+ tf_tree_ipc = { path = "crates/tf_tree_ipc", version = "0.0.7" }
69
+ tf_tree = { path = "crates/tf_tree", version = "0.0.7", default-features = false }
70
70
  # publish = false crates; `default-features = false` as above.
71
71
  tf_tree_bench = { path = "crates/tf_tree_bench", default-features = false }
72
72
  tf_tree_bridge = { path = "crates/tf_tree_bridge" }
@@ -76,6 +76,9 @@ tf_tree_ingest = { path = "crates/tf_tree_ingest", default-features = false }
76
76
  libm = "0.2"
77
77
  bytemuck = { version = "1", features = ["derive"] }
78
78
  blake3 = { version = "1", default-features = false }
79
+ # `tf_tree_math`'s optional `helicoid` feature (`docs/decisions/0063`): off by default, so D14's budget holds for everyone who does not opt in.
80
+ helicoid = "=0.0.4"
81
+ helicoid-linalg = "=0.0.4"
79
82
  # std-side facade / tooling only.
80
83
  thiserror = "2"
81
84
  anyhow = "1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: transform_tree
3
- Version: 0.0.6
3
+ Version: 0.0.7
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Programming Language :: Python :: 3.10
6
6
  Classifier: Programming Language :: Python :: 3.13
@@ -42,9 +42,16 @@ rustdoc-args = ["--cfg", "docsrs"]
42
42
  # `sqrt`/`sin`/`cos` on the hot path — with soft floats.
43
43
  miri-soft-float = ["libm/force-soft-floats"]
44
44
 
45
+ # Delegate the SO(3) bodies to `helicoid` (`docs/decisions/0063`), on by default (`0064`);
46
+ # `--no-default-features` runs the old bodies, which stay compiled either way as the adapter twins.
47
+ default = ["helicoid"]
48
+ helicoid = ["dep:helicoid", "dep:helicoid-linalg"]
49
+
45
50
  [dependencies]
46
51
  libm.workspace = true
47
52
  bytemuck.workspace = true
53
+ helicoid = { workspace = true, optional = true }
54
+ helicoid-linalg = { workspace = true, optional = true }
48
55
 
49
56
  [dev-dependencies]
50
57
  proptest.workspace = true
@@ -0,0 +1,284 @@
1
+ //! The SO(3) bodies delegated to `helicoid` under the `helicoid` feature (`docs/decisions/0063`).
2
+ //!
3
+ //! Each function converts at the boundary and calls one `helicoid` routine; the arithmetic is
4
+ //! `helicoid`'s. `Quat` and `Vec3` keep their layouts, so nothing here crosses a storage contract.
5
+ //!
6
+ //! A quaternion enters on `helicoid`'s *carried* path, a struct literal moved into
7
+ //! `SO3::from_quat_unchecked`, never through the vouching `Quat::from_wxyz_unchecked`. `Iso3`
8
+ //! composition never normalizes, so this crate's quaternions drift by design. The carried path
9
+ //! asserts nothing, propagates NaN, and states each operation's error up to
10
+ //! `|‖q‖² − 1| ≤ 2^-26.29` (`helicoid`'s `NUMERICS.md` §12).
11
+
12
+ use crate::iso3::Vec3;
13
+ use crate::quat::Quat;
14
+ use helicoid::{LieGroup, Quat as HQuat, SO3Tangent, SO3};
15
+ use helicoid_linalg::{Matrix, Vector};
16
+
17
+ #[inline]
18
+ fn so3(q: Quat) -> SO3<f64> {
19
+ SO3::from_quat_unchecked(HQuat {
20
+ w: q.w,
21
+ x: q.x,
22
+ y: q.y,
23
+ z: q.z,
24
+ })
25
+ }
26
+
27
+ #[inline]
28
+ fn quat(r: SO3<f64>) -> Quat {
29
+ let q = r.quat();
30
+ Quat::new(q.w, q.x, q.y, q.z)
31
+ }
32
+
33
+ #[inline]
34
+ pub(crate) fn exp_so3(w: Vec3) -> Quat {
35
+ quat(SO3::exp(&SO3Tangent {
36
+ phi: Vector([w.x, w.y, w.z]),
37
+ }))
38
+ }
39
+
40
+ #[inline]
41
+ pub(crate) fn log_so3(q: Quat) -> Vec3 {
42
+ let [x, y, z] = so3(q).log().phi.0;
43
+ Vec3::new(x, y, z)
44
+ }
45
+
46
+ /// Shepperd's quotients in the native body's arithmetic on a rotation, normalized off the sphere,
47
+ /// as `SO3::from_matrix` returns them (`docs/decisions/0063` decision 3).
48
+ #[inline]
49
+ pub(crate) fn quat_from_rot3(r: &[f64; 9]) -> Quat {
50
+ let row = |i: usize| Vector([r[3 * i], r[3 * i + 1], r[3 * i + 2]]);
51
+ quat(SO3::from_matrix(&Matrix::from_rows([
52
+ row(0),
53
+ row(1),
54
+ row(2),
55
+ ])))
56
+ }
57
+
58
+ #[inline]
59
+ pub(crate) fn slerp(qa: Quat, qb: Quat, s: f64) -> Quat {
60
+ quat(SO3::geodesic(&so3(qa), &so3(qb), s))
61
+ }
62
+
63
+ /// Bit-identical to the native sandwich, drifted or not: the same products, in the same order.
64
+ #[inline]
65
+ pub(crate) fn rotate(q: Quat, v: Vec3) -> Vec3 {
66
+ let [x, y, z] = so3(q).act(Vector([v.x, v.y, v.z])).0;
67
+ Vec3::new(x, y, z)
68
+ }
69
+
70
+ /// The adapter twins (`docs/decisions/0063` decision 5): each delegated function against the body
71
+ /// that ran before delegation, on a deterministic sweep. Tolerances are measured, not derived: the
72
+ /// worst observed distance is asserted as a band, so an unrelated change does not fail them.
73
+ #[cfg(test)]
74
+ mod twin_tests {
75
+ use super::*;
76
+ use crate::interp::slerp_native;
77
+ use crate::quat::{exp_so3_native, log_so3_native, quat_from_rot3_native};
78
+
79
+ /// Rotation-vector magnitudes: both sides of `EXP_SO3_SMALL`, the series switches, and `π`.
80
+ const THETAS: [f64; 14] = [
81
+ 0.0,
82
+ 1e-12,
83
+ 1e-9,
84
+ 1e-8,
85
+ 1e-6,
86
+ 1e-3,
87
+ 0.05,
88
+ 0.15,
89
+ 0.5,
90
+ 1.0,
91
+ 2.0,
92
+ 3.0,
93
+ 3.1,
94
+ core::f64::consts::PI - 1e-9,
95
+ ];
96
+
97
+ /// A unit direction, varied across `k` (golden-ratio steps; no `rand`).
98
+ fn axis(k: usize) -> Vec3 {
99
+ let a = 0.618_033_988_749_894_9 * (k as f64 + 1.0);
100
+ let (u, v) = (
101
+ a.fract() * 2.0 - 1.0,
102
+ (a * 7.0).fract() * core::f64::consts::TAU,
103
+ );
104
+ let r = (1.0 - u * u).sqrt();
105
+ Vec3::new(r * v.cos(), r * v.sin(), u)
106
+ }
107
+
108
+ fn rotation_vectors() -> impl Iterator<Item = Vec3> {
109
+ THETAS
110
+ .iter()
111
+ .enumerate()
112
+ .flat_map(|(i, &t)| (0..8).map(move |k| axis(7 * i + k).scale(t)))
113
+ }
114
+
115
+ fn dist(a: Quat, b: Quat) -> f64 {
116
+ a.sub(b).norm().min(a.add(b).norm())
117
+ }
118
+
119
+ /// `q` as the row-major `[f64; 9]` `quat_from_rot3` consumes.
120
+ fn rot3(q: Quat) -> [f64; 9] {
121
+ let (w, x, y, z) = (q.w, q.x, q.y, q.z);
122
+ [
123
+ 1.0 - 2.0 * (y * y + z * z),
124
+ 2.0 * (x * y - w * z),
125
+ 2.0 * (x * z + w * y),
126
+ 2.0 * (x * y + w * z),
127
+ 1.0 - 2.0 * (x * x + z * z),
128
+ 2.0 * (y * z - w * x),
129
+ 2.0 * (x * z - w * y),
130
+ 2.0 * (y * z + w * x),
131
+ 1.0 - 2.0 * (x * x + y * y),
132
+ ]
133
+ }
134
+
135
+ #[test]
136
+ fn exp_so3_agrees_with_the_native_twin() {
137
+ let worst = rotation_vectors()
138
+ .map(|w| exp_so3(w).sub(exp_so3_native(w)).norm())
139
+ .fold(0.0, f64::max);
140
+ assert!(worst < 1e-15, "exp_so3 differs by {worst:e}");
141
+ }
142
+
143
+ #[test]
144
+ fn log_so3_agrees_with_the_native_twin() {
145
+ let worst = rotation_vectors()
146
+ .map(|w| {
147
+ let q = exp_so3_native(w);
148
+ log_so3(q).sub(log_so3_native(q)).norm()
149
+ })
150
+ .fold(0.0, f64::max);
151
+ assert!(worst < 1e-14, "log_so3 differs by {worst:e}");
152
+ }
153
+
154
+ /// The recorded behaviour change (`0063` decision 3): the native result is not normalized, so
155
+ /// the twin is compared after `normalize`, and up to the sign of the quaternion.
156
+ #[test]
157
+ fn quat_from_rot3_agrees_with_the_normalized_native_twin() {
158
+ let worst = rotation_vectors()
159
+ .map(|w| {
160
+ let r = rot3(exp_so3_native(w));
161
+ dist(quat_from_rot3(&r), quat_from_rot3_native(&r).normalize())
162
+ })
163
+ .fold(0.0, f64::max);
164
+ assert!(worst < 1e-14, "quat_from_rot3 differs by {worst:e}");
165
+ }
166
+
167
+ #[test]
168
+ fn slerp_agrees_with_the_native_twin() {
169
+ let qs: [Quat; 4] =
170
+ core::array::from_fn(|i| exp_so3_native(axis(i).scale(0.7 * i as f64 + 0.01)));
171
+ let mut worst = 0.0f64;
172
+ for (i, &qa) in qs.iter().enumerate() {
173
+ for &qb in &qs[i + 1..] {
174
+ for s in [0.0, 0.1, 0.25, 0.5, 0.9, 1.0] {
175
+ worst = worst.max(dist(slerp(qa, qb, s), slerp_native(qa, qb, s)));
176
+ }
177
+ }
178
+ }
179
+ assert!(worst < 1e-14, "slerp differs by {worst:e}");
180
+ }
181
+
182
+ #[test]
183
+ fn slerp_endpoints_agree_with_the_native_twin() {
184
+ let qa = exp_so3_native(axis(3).scale(0.4));
185
+ for theta in [1e-8, 1e-3, 0.9] {
186
+ let qb = qa * exp_so3_native(axis(5).scale(theta));
187
+ assert!(
188
+ dist(slerp(qa, qb, 0.0), qa) < 4e-16,
189
+ "s = 0, theta {theta:e}"
190
+ );
191
+ assert!(
192
+ dist(slerp(qa, qb, 1.0), qb) < 4e-16,
193
+ "s = 1, theta {theta:e}"
194
+ );
195
+ }
196
+ }
197
+
198
+ #[test]
199
+ fn slerp_of_identical_inputs_is_qa_to_a_few_ulp() {
200
+ for k in 0..16 {
201
+ let qa = exp_so3_native(axis(k).scale(0.3 * k as f64 + 0.05));
202
+ for j in 0..=10 {
203
+ let d = dist(slerp(qa, qa, j as f64 / 10.0), qa);
204
+ assert!(d < 1e-15, "k={k} j={j}: {d:e}");
205
+ }
206
+ }
207
+ }
208
+
209
+ /// The native series collapses outside `[0, 1]`; `helicoid`'s geodesic is `x0 ⊕ s d`, so it holds.
210
+ #[test]
211
+ fn slerp_extrapolates_off_the_segment() {
212
+ let mut worst = 0.0f64;
213
+ for k in 0..16 {
214
+ let qa = exp_so3_native(axis(k).scale(0.3 * k as f64 + 0.05));
215
+ let ax = axis(k + 40);
216
+ for theta in [0.02, 0.1, 0.5] {
217
+ let qb = qa * exp_so3_native(ax.scale(theta));
218
+ for s in [-20.0, -5.0, 2.0, 5.0, 20.0] {
219
+ let want = qa * exp_so3_native(ax.scale(s * theta));
220
+ worst = worst.max(dist(slerp(qa, qb, s), want));
221
+ }
222
+ }
223
+ }
224
+ assert!(worst < 1e-13, "extrapolation differs by {worst:e}");
225
+ }
226
+
227
+ /// `q` scaled so that `‖q‖² − 1 = eta`: the drift `Iso3` composition carries.
228
+ fn drifted(q: Quat, eta: f64) -> Quat {
229
+ q.scale((1.0 + eta).sqrt())
230
+ }
231
+
232
+ /// Unit, at `helicoid`'s vouched bound `2^-40`, and out to the edge of its drift band.
233
+ const DRIFTS: [f64; 5] = [0.0, 9.1e-13, -9.1e-13, 1.2e-8, -1.2e-8];
234
+
235
+ #[test]
236
+ fn rotate_is_the_native_twin_to_the_bit_drifted_or_not() {
237
+ let v = [
238
+ Vec3::new(1.0, -2.0, 0.5),
239
+ Vec3::new(-3e-9, 7.0, 1e6),
240
+ Vec3::new(0.0, 0.0, 1.0),
241
+ ];
242
+ let bits = |a: Vec3| [a.x, a.y, a.z].map(f64::to_bits);
243
+ for w in rotation_vectors() {
244
+ for eta in DRIFTS {
245
+ let q = drifted(exp_so3_native(w), eta);
246
+ for &v in &v {
247
+ assert_eq!(
248
+ bits(rotate(q, v)),
249
+ bits(q.rotate_native(v)),
250
+ "{w:?} {eta:e}"
251
+ );
252
+ }
253
+ }
254
+ }
255
+ }
256
+
257
+ /// A drifted quaternion reaches every delegated function without a debug panic, and `log_so3`
258
+ /// stays the native twin's: both are scale-invariant.
259
+ #[test]
260
+ fn a_drifted_quaternion_is_carried_through_every_delegated_function() {
261
+ let mut worst = 0.0f64;
262
+ for w in rotation_vectors() {
263
+ for eta in DRIFTS {
264
+ let q = drifted(exp_so3_native(w), eta);
265
+ worst = worst.max(log_so3(q).sub(log_so3_native(q)).norm());
266
+ let other = drifted(exp_so3_native(w.scale(0.5)), -eta);
267
+ let _ = slerp(q, other, 0.3);
268
+ }
269
+ }
270
+ assert!(worst < 1e-14, "log_so3 of a drifted q differs by {worst:e}");
271
+ }
272
+
273
+ #[test]
274
+ fn nan_propagates_through_every_delegated_function() {
275
+ let q = Quat::new(f64::NAN, 0.1, 0.2, 0.3);
276
+ let one = exp_so3_native(Vec3::new(0.3, -0.2, 0.1));
277
+ let log = log_so3(q);
278
+ assert!(log.x.is_nan() && log.y.is_nan() && log.z.is_nan());
279
+ assert!(rotate(q, Vec3::new(1.0, 2.0, 3.0)).x.is_nan());
280
+ assert!(slerp(q, one, 0.5).w.is_nan());
281
+ assert!(slerp(one, q, 0.5).w.is_nan());
282
+ assert!(slerp(one, one, f64::NAN).w.is_nan());
283
+ }
284
+ }
@@ -159,6 +159,21 @@ impl Iso3 {
159
159
  #[inline]
160
160
  #[must_use]
161
161
  pub fn slerp(qa: Quat, qb: Quat, s: f64) -> Quat {
162
+ #[cfg(feature = "helicoid")]
163
+ {
164
+ crate::helicoid_backend::slerp(qa, qb, s)
165
+ }
166
+ #[cfg(not(feature = "helicoid"))]
167
+ {
168
+ slerp_native(qa, qb, s)
169
+ }
170
+ }
171
+
172
+ /// The body that ran before delegation, kept as [`slerp`]'s adapter twin (`docs/decisions/0063`).
173
+ #[inline]
174
+ #[must_use]
175
+ #[cfg_attr(all(feature = "helicoid", not(test)), allow(dead_code))]
176
+ pub(crate) fn slerp_native(qa: Quat, qb: Quat, s: f64) -> Quat {
162
177
  let dot = qa.dot(qb);
163
178
  let qb = if dot < 0.0 { qb.neg() } else { qb };
164
179
 
@@ -33,6 +33,8 @@
33
33
  mod readme {}
34
34
 
35
35
  pub mod dualquat;
36
+ #[cfg(feature = "helicoid")]
37
+ mod helicoid_backend;
36
38
  pub mod interp;
37
39
  pub mod iso3;
38
40
  pub mod quat;
@@ -174,6 +174,22 @@ impl Quat {
174
174
  #[inline]
175
175
  #[must_use]
176
176
  pub fn rotate(self, v: Vec3) -> Vec3 {
177
+ #[cfg(feature = "helicoid")]
178
+ {
179
+ crate::helicoid_backend::rotate(self, v)
180
+ }
181
+ #[cfg(not(feature = "helicoid"))]
182
+ {
183
+ self.rotate_native(v)
184
+ }
185
+ }
186
+
187
+ /// The body that ran before delegation, kept as [`Quat::rotate`]'s adapter twin
188
+ /// (`docs/decisions/0063`).
189
+ #[inline]
190
+ #[must_use]
191
+ #[cfg_attr(all(feature = "helicoid", not(test)), allow(dead_code))]
192
+ pub(crate) fn rotate_native(self, v: Vec3) -> Vec3 {
177
193
  let u = self.vector();
178
194
  let t = u.cross(v).scale(2.0);
179
195
  v.add(t.scale(self.w)).add(u.cross(t))
@@ -205,6 +221,21 @@ impl Mul for Quat {
205
221
  #[inline]
206
222
  #[must_use]
207
223
  pub fn exp_so3(w: Vec3) -> Quat {
224
+ #[cfg(feature = "helicoid")]
225
+ {
226
+ crate::helicoid_backend::exp_so3(w)
227
+ }
228
+ #[cfg(not(feature = "helicoid"))]
229
+ {
230
+ exp_so3_native(w)
231
+ }
232
+ }
233
+
234
+ /// The body that ran before delegation, kept as [`exp_so3`]'s adapter twin (`docs/decisions/0063`).
235
+ #[inline]
236
+ #[must_use]
237
+ #[cfg_attr(all(feature = "helicoid", not(test)), allow(dead_code))]
238
+ pub(crate) fn exp_so3_native(w: Vec3) -> Quat {
208
239
  exp_so3_theta(w, w.norm())
209
240
  }
210
241
 
@@ -238,6 +269,21 @@ pub(crate) fn exp_so3_theta(w: Vec3, theta: f64) -> Quat {
238
269
  #[inline]
239
270
  #[must_use]
240
271
  pub fn log_so3(q: Quat) -> Vec3 {
272
+ #[cfg(feature = "helicoid")]
273
+ {
274
+ crate::helicoid_backend::log_so3(q)
275
+ }
276
+ #[cfg(not(feature = "helicoid"))]
277
+ {
278
+ log_so3_native(q)
279
+ }
280
+ }
281
+
282
+ /// The body that ran before delegation, kept as [`log_so3`]'s adapter twin (`docs/decisions/0063`).
283
+ #[inline]
284
+ #[must_use]
285
+ #[cfg_attr(all(feature = "helicoid", not(test)), allow(dead_code))]
286
+ pub(crate) fn log_so3_native(q: Quat) -> Vec3 {
241
287
  // w ≥ 0 puts the result in the principal branch.
242
288
  let (w, x, y, z) = if q.w < 0.0 {
243
289
  (-q.w, -q.x, -q.y, -q.z)
@@ -269,6 +315,21 @@ pub fn log_so3(q: Quat) -> Vec3 {
269
315
  #[inline]
270
316
  #[must_use]
271
317
  pub fn quat_from_rot3(r: &[f64; 9]) -> Quat {
318
+ #[cfg(feature = "helicoid")]
319
+ {
320
+ crate::helicoid_backend::quat_from_rot3(r)
321
+ }
322
+ #[cfg(not(feature = "helicoid"))]
323
+ {
324
+ quat_from_rot3_native(r)
325
+ }
326
+ }
327
+
328
+ /// The body that ran before delegation, kept as [`quat_from_rot3`]'s adapter twin (`docs/decisions/0063`).
329
+ #[inline]
330
+ #[must_use]
331
+ #[cfg_attr(all(feature = "helicoid", not(test)), allow(dead_code))]
332
+ pub(crate) fn quat_from_rot3_native(r: &[f64; 9]) -> Quat {
272
333
  let (r00, r11, r22) = (r[0], r[4], r[8]);
273
334
  let trace = r00 + r11 + r22;
274
335
  if trace > 0.0 {
@@ -111,7 +111,11 @@ fn endpoints_are_bit_exact_above_the_lerp_fallback() {
111
111
  /// `s = 0` differs from `qa` by a couple of ulp unless `qa` is already unit to
112
112
  /// the bit. That ulp is the observable: it appears below the threshold and
113
113
  /// disappears above it with the same `qa`.
114
+ ///
115
+ /// Native arm only: `helicoid`'s geodesic has no LERP fallback to pin, and its endpoints are
116
+ /// held by `helicoid_backend`'s `slerp_endpoints_agree_with_the_native_twin`.
114
117
  #[test]
118
+ #[cfg(not(feature = "helicoid"))]
115
119
  fn endpoints_lose_bit_exactness_only_in_the_lerp_fallback() {
116
120
  let mut discriminating = 0usize;
117
121
  let mut worst = 0.0f64;
@@ -262,7 +266,10 @@ fn the_arc_is_the_short_one_and_the_sign_of_qb_is_irrelevant() {
262
266
  }
263
267
 
264
268
  /// Identical inputs short-circuit, and `s` cannot perturb the answer.
269
+ ///
270
+ /// Native arm only: `helicoid`'s geodesic returns `qa` to a few ulp, not the bit; `slerp_of_identical_inputs_is_qa_to_a_few_ulp` pins that arm.
265
271
  #[test]
272
+ #[cfg(not(feature = "helicoid"))]
266
273
  fn numerically_identical_inputs_return_qa_at_every_s() {
267
274
  for k in 0..16 {
268
275
  let qa = sample_rotation(k);
@@ -274,11 +281,13 @@ fn numerically_identical_inputs_return_qa_at_every_s() {
274
281
  }
275
282
 
276
283
  /// The exact great-circle point at parameter `s`, valid off the segment too.
284
+ #[cfg(not(feature = "helicoid"))]
277
285
  fn geodesic(qa: Quat, theta: f64, s: f64) -> Quat {
278
286
  step(qa, s * theta)
279
287
  }
280
288
 
281
289
  /// The double cover: the distance that counts is the smaller of the two.
290
+ #[cfg(not(feature = "helicoid"))]
282
291
  fn rot_dist(a: Quat, b: Quat) -> f64 {
283
292
  a.sub(b).norm().min(a.add(b).norm())
284
293
  }
@@ -287,7 +296,10 @@ fn rot_dist(a: Quat, b: Quat) -> f64 {
287
296
  /// branches degrade differently (closed form holds, series collapses, the
288
297
  /// fallback's chord is fine until the arc grows), so the bands are wide and pin
289
298
  /// the shape, not a libm revision.
299
+ ///
300
+ /// Native arm only: `helicoid` does not collapse out of range; `slerp_extrapolates_off_the_segment` pins that arm.
290
301
  #[test]
302
+ #[cfg(not(feature = "helicoid"))]
291
303
  fn out_of_range_s_extrapolates_and_only_the_closed_form_holds() {
292
304
  // Closed form: as true off the segment as on it.
293
305
  let mut worst_closed = 0.0f64;
@@ -364,15 +376,14 @@ fn out_of_range_s_extrapolates_and_only_the_closed_form_holds() {
364
376
  );
365
377
  }
366
378
 
367
- /// `NaN` propagates through every branch except the early return that answers
368
- /// before `s` is read: `slerp(qa, qa, NaN)` returns `qa`.
379
+ /// `NaN` propagates through every branch, on both arms.
369
380
  ///
370
381
  /// Do not "harden" the closed form. A `NaN` component reaches the closed form,
371
382
  /// where `acos(NaN.min(1.0)) = 0` makes `sin_angle` zero and the output `NaN` only
372
383
  /// via `0.0/0.0`; a `sin_angle == 0.0` guard would turn a `NaN` input into a
373
384
  /// plausible pose. `dot.clamp(-1.0, 1.0)` would be safe; see `slerp`'s docs.
374
385
  #[test]
375
- fn nan_propagates_except_through_the_identical_input_return() {
386
+ fn nan_propagates_through_every_branch() {
376
387
  let all_nan = |q: Quat| q.w.is_nan() && q.x.is_nan() && q.y.is_nan() && q.z.is_nan();
377
388
  for k in 0..16 {
378
389
  let qa = sample_rotation(k);
@@ -397,18 +408,29 @@ fn nan_propagates_except_through_the_identical_input_return() {
397
408
  before relaxing this."
398
409
  );
399
410
  }
400
- // `h` is `0`, so nothing downstream reads `s`.
401
- assert_eq!(bits(slerp(qa, qa, f64::NAN)), bits(qa), "k={k}");
402
- assert_eq!(bits(slerp(qa, qa, f64::INFINITY)), bits(qa), "k={k}");
403
- // A NaN component cannot reach it: `h` is NaN, not `<= 0.0`.
404
411
  let qnan = Quat::new(f64::NAN, qa.x, qa.y, qa.z);
405
412
  assert!(
406
413
  all_nan(slerp(qnan, qnan, 0.5)),
407
- "the early return ate a NaN component: k={k}"
414
+ "a NaN component was eaten: k={k}"
408
415
  );
409
416
  }
410
417
  }
411
418
 
419
+ /// The native early return answers before `s` is read: `slerp(qa, qa, NaN)` returns `qa`. A NaN
420
+ /// component cannot reach it, which `nan_propagates_through_every_branch` checks: `h` is NaN, not
421
+ /// `<= 0.0`.
422
+ ///
423
+ /// Native arm only: `helicoid`'s geodesic has no early return, so a NaN `s` gives NaN there.
424
+ #[test]
425
+ #[cfg(not(feature = "helicoid"))]
426
+ fn the_identical_input_return_answers_before_s_is_read() {
427
+ for k in 0..16 {
428
+ let qa = sample_rotation(k);
429
+ assert_eq!(bits(slerp(qa, qa, f64::NAN)), bits(qa), "k={k}");
430
+ assert_eq!(bits(slerp(qa, qa, f64::INFINITY)), bits(qa), "k={k}");
431
+ }
432
+ }
433
+
412
434
  /// What dropping the `Iso3` round trip costs: as rotations `slerp` and
413
435
  /// `LerpSlerp::eval` never disagree (worst ~2.7e-16, the renormalization); the
414
436
  /// bits differ only at `s = 0` or `s = 1`, where `eval` answers from a shortcut:
@@ -231,6 +231,25 @@ version = "0.5.0"
231
231
  source = "registry+https://github.com/rust-lang/crates.io-index"
232
232
  checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
233
233
 
234
+ [[package]]
235
+ name = "helicoid"
236
+ version = "0.0.4"
237
+ source = "registry+https://github.com/rust-lang/crates.io-index"
238
+ checksum = "5d8d5abfd68550c667ec6721da48a1d1fb852b435db673a8fda7093195411a7d"
239
+ dependencies = [
240
+ "helicoid-linalg",
241
+ "libm",
242
+ ]
243
+
244
+ [[package]]
245
+ name = "helicoid-linalg"
246
+ version = "0.0.4"
247
+ source = "registry+https://github.com/rust-lang/crates.io-index"
248
+ checksum = "994bdd3e24ef9a226b96904499395071be521cf75a4a4234198bc71a627794d2"
249
+ dependencies = [
250
+ "libm",
251
+ ]
252
+
234
253
  [[package]]
235
254
  name = "hermit-abi"
236
255
  version = "0.5.2"
@@ -553,7 +572,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
553
572
 
554
573
  [[package]]
555
574
  name = "tf_tree"
556
- version = "0.0.6"
575
+ version = "0.0.7"
557
576
  dependencies = [
558
577
  "tf_tree_arena",
559
578
  "tf_tree_core",
@@ -564,7 +583,7 @@ dependencies = [
564
583
 
565
584
  [[package]]
566
585
  name = "tf_tree_arena"
567
- version = "0.0.6"
586
+ version = "0.0.7"
568
587
  dependencies = [
569
588
  "bytemuck",
570
589
  "rustix",
@@ -572,14 +591,14 @@ dependencies = [
572
591
 
573
592
  [[package]]
574
593
  name = "tf_tree_bridge"
575
- version = "0.0.6"
594
+ version = "0.0.7"
576
595
  dependencies = [
577
596
  "tf_tree",
578
597
  ]
579
598
 
580
599
  [[package]]
581
600
  name = "tf_tree_core"
582
- version = "0.0.6"
601
+ version = "0.0.7"
583
602
  dependencies = [
584
603
  "blake3",
585
604
  "bytemuck",
@@ -590,7 +609,7 @@ dependencies = [
590
609
 
591
610
  [[package]]
592
611
  name = "tf_tree_ingest"
593
- version = "0.0.6"
612
+ version = "0.0.7"
594
613
  dependencies = [
595
614
  "blake3",
596
615
  "crc32fast",
@@ -604,7 +623,7 @@ dependencies = [
604
623
 
605
624
  [[package]]
606
625
  name = "tf_tree_ipc"
607
- version = "0.0.6"
626
+ version = "0.0.7"
608
627
  dependencies = [
609
628
  "libc",
610
629
  "rustix",
@@ -612,15 +631,17 @@ dependencies = [
612
631
 
613
632
  [[package]]
614
633
  name = "tf_tree_math"
615
- version = "0.0.6"
634
+ version = "0.0.7"
616
635
  dependencies = [
617
636
  "bytemuck",
637
+ "helicoid",
638
+ "helicoid-linalg",
618
639
  "libm",
619
640
  ]
620
641
 
621
642
  [[package]]
622
643
  name = "tf_tree_py"
623
- version = "0.0.6"
644
+ version = "0.0.7"
624
645
  dependencies = [
625
646
  "numpy",
626
647
  "pyo3",