transform-tree 0.0.4__tar.gz → 0.0.5__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 (156) hide show
  1. {transform_tree-0.0.4 → transform_tree-0.0.5}/Cargo.toml +6 -6
  2. {transform_tree-0.0.4 → transform_tree-0.0.5}/PKG-INFO +263 -48
  3. transform_tree-0.0.5/README.md +515 -0
  4. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/Cargo.toml +11 -0
  5. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/README.md +29 -3
  6. transform_tree-0.0.5/crates/tf_tree/examples/control_loop.rs +290 -0
  7. transform_tree-0.0.5/crates/tf_tree/examples/gen_domain_fixture.rs +131 -0
  8. transform_tree-0.0.5/crates/tf_tree/examples/two_processes.rs +241 -0
  9. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/src/bin/rendezvous_child.rs +151 -2
  10. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/src/cache.rs +517 -24
  11. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/src/lib.rs +14 -3
  12. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/src/open.rs +418 -310
  13. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/src/tree.rs +1101 -104
  14. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/tests/batch.rs +39 -0
  15. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/tests/behavior.rs +38 -0
  16. transform_tree-0.0.5/crates/tf_tree/tests/clock_offset.rs +447 -0
  17. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/tests/frozen.rs +76 -0
  18. transform_tree-0.0.5/crates/tf_tree/tests/lookup.rs +333 -0
  19. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/tests/rendezvous.rs +2095 -44
  20. transform_tree-0.0.5/crates/tf_tree/tests/wide_stamps.rs +218 -0
  21. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_arena/README.md +4 -0
  22. transform_tree-0.0.5/crates/tf_tree_bridge/Cargo.toml +68 -0
  23. transform_tree-0.0.5/crates/tf_tree_bridge/examples/offer_cost.rs +268 -0
  24. transform_tree-0.0.5/crates/tf_tree_bridge/src/authority.rs +549 -0
  25. transform_tree-0.0.5/crates/tf_tree_bridge/src/clock.rs +1333 -0
  26. transform_tree-0.0.5/crates/tf_tree_bridge/src/config.rs +1906 -0
  27. transform_tree-0.0.5/crates/tf_tree_bridge/src/discover.rs +568 -0
  28. transform_tree-0.0.5/crates/tf_tree_bridge/src/edgeindex.rs +430 -0
  29. transform_tree-0.0.5/crates/tf_tree_bridge/src/edgemap.rs +54 -0
  30. transform_tree-0.0.5/crates/tf_tree_bridge/src/ingest.rs +3407 -0
  31. transform_tree-0.0.5/crates/tf_tree_bridge/src/interner.rs +178 -0
  32. transform_tree-0.0.5/crates/tf_tree_bridge/src/lib.rs +380 -0
  33. transform_tree-0.0.5/crates/tf_tree_bridge/src/names.rs +306 -0
  34. transform_tree-0.0.5/crates/tf_tree_bridge/src/statics.rs +468 -0
  35. transform_tree-0.0.5/crates/tf_tree_bridge/src/stats.rs +200 -0
  36. transform_tree-0.0.5/crates/tf_tree_bridge/tests/steady_state_alloc.rs +420 -0
  37. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_core/Cargo.toml +47 -0
  38. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_core/README.md +4 -0
  39. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_core/src/arena_view.rs +94 -0
  40. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_core/src/buffer.rs +25 -0
  41. transform_tree-0.0.5/crates/tf_tree_core/src/crash.rs +167 -0
  42. transform_tree-0.0.5/crates/tf_tree_core/src/crash_tests.rs +702 -0
  43. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_core/src/edge.rs +101 -6
  44. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_core/src/error.rs +266 -2
  45. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_core/src/frame.rs +44 -0
  46. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_core/src/layout.rs +20 -10
  47. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_core/src/lib.rs +71 -9
  48. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_core/src/loom_tests.rs +103 -20
  49. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_core/src/participant.rs +42 -4
  50. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_core/src/plan.rs +784 -193
  51. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_core/src/sample.rs +189 -14
  52. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_core/src/tests.rs +1283 -36
  53. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_core/src/topology.rs +29 -0
  54. transform_tree-0.0.5/crates/tf_tree_ingest/Cargo.toml +187 -0
  55. transform_tree-0.0.5/crates/tf_tree_ingest/examples/gen_zstd_conformance.rs +196 -0
  56. transform_tree-0.0.5/crates/tf_tree_ingest/src/cdr.rs +533 -0
  57. transform_tree-0.0.5/crates/tf_tree_ingest/src/decompress.rs +2268 -0
  58. transform_tree-0.0.5/crates/tf_tree_ingest/src/fixture.rs +1956 -0
  59. transform_tree-0.0.5/crates/tf_tree_ingest/src/ingest.rs +1410 -0
  60. transform_tree-0.0.5/crates/tf_tree_ingest/src/lib.rs +477 -0
  61. transform_tree-0.0.5/crates/tf_tree_ingest/src/report.rs +628 -0
  62. transform_tree-0.0.5/crates/tf_tree_ingest/src/source.rs +826 -0
  63. transform_tree-0.0.5/crates/tf_tree_ingest/src/spill.rs +761 -0
  64. transform_tree-0.0.5/crates/tf_tree_ingest/src/tft.rs +51 -0
  65. transform_tree-0.0.5/crates/tf_tree_ingest/testdata/ATTRIBUTION.md +103 -0
  66. transform_tree-0.0.5/crates/tf_tree_ingest/testdata/zstd_conformance.mcap +0 -0
  67. transform_tree-0.0.5/crates/tf_tree_ingest/tests/codec_free.rs +175 -0
  68. transform_tree-0.0.5/crates/tf_tree_ingest/tests/frozen_bag.rs +107 -0
  69. transform_tree-0.0.5/crates/tf_tree_ingest/tests/ingest.rs +2955 -0
  70. transform_tree-0.0.5/crates/tf_tree_ingest/tests/memory.rs +145 -0
  71. transform_tree-0.0.5/crates/tf_tree_ingest/tests/record_ceiling.rs +135 -0
  72. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_ipc/README.md +4 -0
  73. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_ipc/src/bin/ipc_child.rs +0 -1
  74. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_ipc/src/client.rs +48 -1
  75. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_ipc/src/error.rs +71 -5
  76. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_ipc/src/identity.rs +158 -9
  77. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_ipc/src/lib.rs +5 -2
  78. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_ipc/src/lockfile.rs +145 -8
  79. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_ipc/src/open.rs +305 -78
  80. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_ipc/src/procstat.rs +153 -3
  81. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_ipc/src/server.rs +44 -1
  82. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_ipc/tests/multiprocess.rs +90 -0
  83. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_math/README.md +11 -0
  84. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_math/src/iso3.rs +36 -10
  85. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_math/src/lib.rs +12 -2
  86. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_py/Cargo.lock +232 -6
  87. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_py/Cargo.toml +57 -4
  88. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_py/src/errors.rs +93 -21
  89. transform_tree-0.0.5/crates/tf_tree_py/src/ingest.rs +187 -0
  90. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_py/src/lib.rs +66 -7
  91. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_py/src/offline.rs +11 -8
  92. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_py/src/tree.rs +1037 -66
  93. {transform_tree-0.0.4 → transform_tree-0.0.5}/pyproject.toml +1 -1
  94. {transform_tree-0.0.4 → transform_tree-0.0.5}/python/tf_tree/__init__.py +19 -0
  95. {transform_tree-0.0.4 → transform_tree-0.0.5}/python/tf_tree/_core.pyi +288 -14
  96. transform_tree-0.0.4/README.md +0 -300
  97. transform_tree-0.0.4/crates/tf_tree/tests/lookup.rs +0 -177
  98. {transform_tree-0.0.4 → transform_tree-0.0.5}/LICENSE-APACHE +0 -0
  99. {transform_tree-0.0.4 → transform_tree-0.0.5}/LICENSE-MIT +0 -0
  100. {transform_tree-0.0.4 → transform_tree-0.0.5}/NOTICE +0 -0
  101. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/LICENSE-APACHE +0 -0
  102. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/LICENSE-MIT +0 -0
  103. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/NOTICE +0 -0
  104. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/src/cbor.rs +0 -0
  105. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/src/frozen.rs +0 -0
  106. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/src/unstable.rs +0 -0
  107. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/tests/await_frames.rs +0 -0
  108. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/tests/common/mod.rs +0 -0
  109. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/tests/construction.rs +0 -0
  110. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/tests/counters.rs +0 -0
  111. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/tests/derivatives.rs +0 -0
  112. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/tests/enumeration.rs +0 -0
  113. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/tests/feature_gates.rs +0 -0
  114. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/tests/math_reexports.rs +0 -0
  115. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/tests/owned_writer.rs +0 -0
  116. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/tests/plan_cache_identity.rs +0 -0
  117. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree/tests/tsan.rs +0 -0
  118. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_arena/Cargo.toml +0 -0
  119. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_arena/LICENSE-APACHE +0 -0
  120. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_arena/LICENSE-MIT +0 -0
  121. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_arena/NOTICE +0 -0
  122. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_arena/src/check.rs +0 -0
  123. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_arena/src/frozen.rs +0 -0
  124. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_arena/src/header.rs +0 -0
  125. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_arena/src/heap.rs +0 -0
  126. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_arena/src/layout.rs +0 -0
  127. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_arena/src/lib.rs +0 -0
  128. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_arena/src/mapped.rs +0 -0
  129. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_arena/tests/heap_alignment.rs +0 -0
  130. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_core/LICENSE-APACHE +0 -0
  131. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_core/LICENSE-MIT +0 -0
  132. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_core/NOTICE +0 -0
  133. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_core/src/bench_probe.rs +0 -0
  134. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_core/src/counters.rs +0 -0
  135. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_core/src/sync.rs +0 -0
  136. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_ipc/Cargo.toml +0 -0
  137. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_ipc/LICENSE-APACHE +0 -0
  138. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_ipc/LICENSE-MIT +0 -0
  139. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_ipc/NOTICE +0 -0
  140. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_ipc/src/fork.rs +0 -0
  141. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_ipc/src/ofd.rs +0 -0
  142. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_ipc/src/rendezvous.rs +0 -0
  143. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_ipc/src/runtime_dir.rs +0 -0
  144. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_ipc/src/wire.rs +0 -0
  145. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_math/Cargo.toml +0 -0
  146. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_math/LICENSE-APACHE +0 -0
  147. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_math/LICENSE-MIT +0 -0
  148. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_math/NOTICE +0 -0
  149. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_math/src/dualquat.rs +0 -0
  150. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_math/src/interp.rs +0 -0
  151. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_math/src/quat.rs +0 -0
  152. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_math/src/reference.rs +0 -0
  153. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_math/src/twist.rs +0 -0
  154. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_math/tests/proptests.rs +0 -0
  155. {transform_tree-0.0.4 → transform_tree-0.0.5}/crates/tf_tree_math/tests/slerp_public.rs +0 -0
  156. {transform_tree-0.0.4 → transform_tree-0.0.5}/python/tf_tree/py.typed +0 -0
@@ -45,7 +45,7 @@ exclude = [
45
45
  # an MSRV raise reaches nobody who did not opt into it by editing a manifest.
46
46
  # The rule comes back into force at 0.1.0, where a minor slot exists again.
47
47
  # `SUPPORT.md`'s MSRV section carries the same statement in its own voice.
48
- version = "0.0.4"
48
+ version = "0.0.5"
49
49
  edition = "2021"
50
50
  # **1.87, and each step was forced by a dependency rather than chosen.**
51
51
  #
@@ -122,8 +122,8 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(loom)'] }
122
122
  [workspace.dependencies]
123
123
  # Intra-workspace crates (version pinned so publishable crates carry a real
124
124
  # requirement, not a path wildcard).
125
- tf_tree_math = { path = "crates/tf_tree_math", version = "0.0.4" }
126
- tf_tree_arena = { path = "crates/tf_tree_arena", version = "0.0.4" }
125
+ tf_tree_math = { path = "crates/tf_tree_math", version = "0.0.5" }
126
+ tf_tree_arena = { path = "crates/tf_tree_arena", version = "0.0.5" }
127
127
  # Phase 2 shared memory (see docs/PHASE2.md §2). Linux-only, opt-in via the
128
128
  # `shm` feature; `linux_raw` backend so there is no libc crate and no C build.
129
129
  # Only `tf_tree_ipc`, and only for `fcntl(F_OFD_*)`: rustix has no OFD locking
@@ -138,9 +138,9 @@ rustix = { version = "1.1", default-features = false, features = ["mm", "fs", "s
138
138
  # measurement — the "counters off" row was measuring the counters-on engine.
139
139
  # Every crate that wants them re-enables them through its own `counters`
140
140
  # feature, which is what makes the switch actually switch.
141
- tf_tree_core = { path = "crates/tf_tree_core", version = "0.0.4", default-features = false }
142
- tf_tree_ipc = { path = "crates/tf_tree_ipc", version = "0.0.4" }
143
- tf_tree = { path = "crates/tf_tree", version = "0.0.4", default-features = false }
141
+ tf_tree_core = { path = "crates/tf_tree_core", version = "0.0.5", default-features = false }
142
+ tf_tree_ipc = { path = "crates/tf_tree_ipc", version = "0.0.5" }
143
+ tf_tree = { path = "crates/tf_tree", version = "0.0.5", default-features = false }
144
144
  # publish = false crates, wired by path only (no version requirement published).
145
145
  # **`default-features = false` for the same load-bearing reason as `tf_tree`
146
146
  # above, and it is easy to miss here.** `tf_tree_bench` has its own
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: transform_tree
3
- Version: 0.0.4
3
+ Version: 0.0.5
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Programming Language :: Python :: 3.10
6
6
  Classifier: Programming Language :: Python :: 3.13
@@ -20,11 +20,40 @@ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
20
20
 
21
21
  # tf_tree
22
22
 
23
+ [![crates.io](https://img.shields.io/crates/v/tf_tree.svg?logo=rust)](https://crates.io/crates/tf_tree)
24
+ [![docs.rs](https://img.shields.io/docsrs/tf_tree?logo=docsdotrs)](https://docs.rs/tf_tree)
25
+ [![PyPI](https://img.shields.io/pypi/v/transform_tree.svg?logo=pypi&logoColor=white)](https://pypi.org/project/transform_tree/)
26
+ [![CI](https://github.com/NoeFontana/tf_tree/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/NoeFontana/tf_tree/actions/workflows/ci.yml)
27
+ [![Licence](https://img.shields.io/badge/licence-MIT%20OR%20Apache--2.0-blue.svg)](#licence)
28
+
23
29
  A transform tree engine: store time-stamped rigid-body transforms between named
24
30
  coordinate frames and answer *"where was frame A relative to frame B at time
25
31
  t?"* — fast enough to sit inside a control loop, with diagnostics good enough to
26
32
  debug at 3 a.m.
27
33
 
34
+ **Jump to:** [Install](#install) · [Start with a bag you already
35
+ have](#start-where-you-change-nothing) · [First five
36
+ minutes](#first-five-minutes-with-no-data-at-all) · [Where it
37
+ fits](#where-it-fits-and-where-it-does-not) · [Status](#status) · [Is this a
38
+ `tf2` replacement?](#is-this-a-tf2-replacement)
39
+
40
+ ## What you get
41
+
42
+ - **A compiled query.** `plan()` resolves the topology and folds static edges
43
+ once. `at()` then does *d* binary searches, *d* interpolations and *d−1*
44
+ compositions, and nothing else — no allocation, no hashing, no lock. Frames
45
+ are interned to integer ids at plan time, so no lookup hashes a string.
46
+ - **One tree, not one copy per process.** The arena holds no pointers — every
47
+ internal reference is an offset — so the same bytes map into every reader:
48
+ threads in one process, cooperating processes on a host, or sixteen dataloader
49
+ workers on a frozen `.tft` file. Consumers attach read-only by default, and
50
+ the MMU is what enforces it, not convention.
51
+ - **Failures you can act on.** Every error is a `Copy` identifier that names the
52
+ offending edge as data rather than as a formatted string, and `tf_tree doctor`
53
+ runs the `TFT001`–`TFT019` catalogue against a live arena, a frozen index, or
54
+ an MCAP recording you already have. Nineteen ids are reported; seventeen can
55
+ detect today, and the two that cannot say so rather than reporting a pass.
56
+
28
57
  **Linux-first.** The single-process engine is portable Rust and much of it
29
58
  compiles elsewhere; everything that maps memory — attaching to a live arena, the
30
59
  frozen `.tft` backend, `tf_tree freeze` — is **Linux-only and behind a
@@ -32,19 +61,78 @@ default-off `shm` feature**. That sentence is here rather than in
32
61
  [`SUPPORT.md`](./SUPPORT.md) alone because nobody should meet it as a build
33
62
  error.
34
63
 
64
+ ## Install
65
+
66
+ | What you want | How to get it | Where it comes from |
67
+ |---|---|---|
68
+ | The Rust engine | `cargo add tf_tree` | crates.io |
69
+ | The Python bindings | `pip install transform_tree`, then `import tf_tree` | PyPI |
70
+ | The `tf_tree` CLI | a prebuilt Linux binary from [the latest release](https://github.com/NoeFontana/tf_tree/releases/latest), or `cargo install --path crates/tf_tree_cli --features shm` from a clone | GitHub Releases (`x86_64`/`aarch64`, gnu and static musl); the CLI is `publish = false`, so not crates.io |
71
+ | C ABI, C++ header, ROS 2 bridge | `just c-abi-check`, `just cpp-check`, `just ros-build` | source |
72
+
73
+ Three notes on that table, each of which surprises somebody:
74
+
75
+ - **The distribution name is not the import name.** PyPI refuses `tf_tree` as
76
+ too close to the existing `tftree`, so the wheel is `transform_tree` and the
77
+ module stays `tf_tree` ([`0008`](./docs/decisions/0008-the-name-tf-tree.md)
78
+ records the measurement).
79
+ - **`cargo add tf_tree` gives you the portable engine.** Shared memory and the
80
+ frozen `.tft` reader need `--features shm`, on Linux.
81
+ - **`cargo install tf_tree` installs no command**, and does not fail either:
82
+ it exits 0 with a warning naming `--features shm`. Adding that flag *does*
83
+ install `tf_tree_rendezvous_child`, which is a test helper and not a tool —
84
+ [the crate's own page](./crates/tf_tree/README.md) has the whole story. The
85
+ CLI is a separate, unpublished crate: take the prebuilt binary from the
86
+ releases page, or build it from a checkout. **`publish = false` is about the
87
+ crates.io index, not about whether it is shipped** — three of its dependencies
88
+ are path-only, so it has no version to publish against.
89
+
90
+ **`0.0.x` promises nothing between releases.** Cargo treats every `0.0.x` as
91
+ incompatible with every other, so pin exactly and expect a later release to
92
+ break. That is the whole promise; see [`CHANGELOG.md`](./CHANGELOG.md).
93
+
35
94
  ## Start where you change nothing
36
95
 
37
96
  Point it at a recording you already have. No node joins anyone's launch file, no
38
97
  robot is redeployed, and `doctor --from-bag` needs no features at all:
39
98
 
40
99
  ```sh
41
- git clone https://github.com/NoeFontana/tf_tree && cd tf_tree
42
- cargo install --path crates/tf_tree_cli --features shm # shm: `freeze` maps memory
100
+ # No clone and no Rust toolchain. The musl build is static: it needs no system
101
+ # library, so it runs on Ubuntu 20.04 and in a distroless container alike.
102
+ TAG=$(curl -fsSL https://api.github.com/repos/NoeFontana/tf_tree/releases/latest \
103
+ | grep -m1 '"tag_name"' | cut -d'"' -f4)
104
+ curl -fsSL "https://github.com/NoeFontana/tf_tree/releases/download/${TAG}/tf_tree-${TAG}-x86_64-unknown-linux-musl.tar.gz" | tar xz
105
+ tft=./tf_tree-${TAG}-x86_64-unknown-linux-musl/tf_tree # stay where your recording is
106
+
107
+ $tft doctor --from-bag drive.mcap # what is wrong with this /tf traffic
108
+ $tft freeze --from-bag drive.mcap -o drive.tft # keep the answer
109
+ ```
110
+
111
+ Both released builds carry `--features shm`, so the same binary also attaches to
112
+ a robot that is already running (`--attach`, `tf_tree top`, `tf_tree
113
+ participants`). The `-gnu` archive is the dynamically-linked build and needs
114
+ glibc 2.34 or newer (Ubuntu 22.04 / ROS 2 Humble and later); take it if you
115
+ would rather a glibc security fix reach this binary through your distribution
116
+ than wait for a new release. Otherwise take musl. From a clone the equivalent is
117
+ `cargo install --path crates/tf_tree_cli --features shm`.
118
+
119
+ The same two steps run from Python, with no CLI and no clone — which is what
120
+ `tf_tree_ingest` was made a library crate for
121
+ ([`0046`](./docs/decisions/0046-the-consumer-the-crate-boundary-was-drawn-for.md)):
43
122
 
44
- tf_tree doctor --from-bag drive.mcap # what is wrong with this /tf traffic
45
- tf_tree freeze --from-bag drive.mcap -o drive.tft # keep the answer
123
+ ```python
124
+ import tf_tree
125
+
126
+ tree = tf_tree.ingest_bag("drive.mcap") # returns the ordinary Tree
127
+ tree.freeze("drive.tft") # records drive.mcap's BLAKE3 digest
128
+ print(tree.source["transforms"], tree.source["digest"][:16])
46
129
  ```
47
130
 
131
+ `tree.source` is how a `.tft` stays traceable to the recording it came from, and
132
+ `freeze` writes that digest for you — so there is no second `freeze_bag` call to
133
+ remember. Taking a publisher on the tree drops it, because from that point the
134
+ tree may hold samples the recording does not.
135
+
48
136
  `drive.tft` is a **frozen transform index**, and it is the arena itself written
49
137
  to disk. There are no pointers anywhere in the arena — every internal reference
50
138
  is an offset — so opening one is an `mmap`, with no parsing, no deserialization
@@ -76,39 +164,28 @@ the ability to query at arbitrary times, or runs a ROS node to serve transforms
76
164
  during training. This replaces all three and asks nobody to migrate anything,
77
165
  which is also why it is the part that shipped first.
78
166
 
167
+ **Running it inside a loop instead?** `just control-loop` is the other half of
168
+ this page — `cargo run --release -p tf_tree --features shm --example control_loop`.
169
+ It is a 1 kHz controller against a 200 Hz estimate, showing the four things a
170
+ runtime consumer has to get right (compile the plan once, hoist the guard,
171
+ extrapolate on purpose and read how far it reached, treat a contended slot as
172
+ data) and printing the tail a deadline is set against. The offline path above
173
+ asks nobody to change their robot; this one is what happens when they do.
174
+
175
+ **Two processes, not two threads?** `just two-processes` — the capability this
176
+ page leads with, as something you can run. It spawns a publisher and a consumer
177
+ as separate processes and prints what each saw: the publisher declares the
178
+ topology (including a *static* sensor mount, which folds to one multiply and
179
+ needs no publisher ever), the consumer waits for it with `Open::await_open`
180
+ rather than depending on launch order, and then reads a transform and how far
181
+ past the newest sample it had to reach.
182
+
79
183
  **Numbers belong where they can be reproduced**, not in this section.
80
184
  `just bench-report` measures your host and writes
81
185
  `report/{results.json,index.html}`; the standing figures and their caveats are in
82
186
  [`docs/benchmarks/`](./docs/benchmarks/). *Benchmarks, and what they are worth*
83
187
  below explains why a row there may legitimately read `UNAVAILABLE`.
84
188
 
85
- ## Status
86
-
87
- **On crates.io from 0.0.1; on PyPI from 0.0.2.** The five engine crates are
88
- published — `cargo add tf_tree`. The Python wheel starts at 0.0.2, because the
89
- 0.0.1 commit did not compile off Linux and no wheel for it exists or can; see
90
- [`CHANGELOG.md`](./CHANGELOG.md). **`0.0.x` promises nothing between releases** —
91
- cargo treats every one as incompatible with every other, so pin exactly.
92
-
93
- | Phase | What it is | Status |
94
- |---|---|---|
95
- | 1 | Single-process engine: arena, seqlock buffers, plans, SE(3) math | **Implemented** |
96
- | 2 | Shared memory: rendezvous, fd passing, claims as leases, reaping | **Implemented**, except the daemon/recorder surface (§9–§10) and §11.3's fault injection. §3.5's ownership migration has the protocol and not the trigger: kill the arena's owner and lookups keep being served, but no new process can join |
97
- | 3 | Python bindings (PyO3, zero intermediate allocation) | **Implemented** |
98
- | 4 | C ABI, C++ wrapper, ROS 2 ingest bridge, derivatives | **Done, except §5.9's affinity knobs and §6.3's replay rows.** `at_with_derivatives`, both headers, the header-only C++ wrapper with its CMake package, and **both halves of the ingest bridge** — the `rclcpp` package in `ros/tf_tree_ros` included. §7's benchmark gate is partial |
99
- | 5 | Frozen `.tft` arena, bag ingestion, diagnostics, `tf_tree top` | **Mostly done.** `FORMAT_VERSION = 3`, the frozen arena (§2), the offline Python API (§4), the §5 counters and `tf_tree top` — terminal *and* `--web` (§7) — all landed. Ingestion is **MCAP only** (§3); the `TFT001`–`TFT019` catalogue reports all nineteen ids, of which sixteen can detect (§6). §8 is **deliberately not built**. §9's benchmark artifact and §10's release readiness are partial |
100
- | 6–8 | Multi-host, `tf2` compatibility shim, replication | Not started; Phase 7 is gated by D21 and none of its four gates is met |
101
-
102
- **The per-phase `§0.0` tables in [`docs/`](./docs/) are the source of truth**, not
103
- this one — [`PHASE2.md`](./docs/PHASE2.md#00-implementation-status),
104
- [`PHASE4.md`](./docs/PHASE4.md#00-implementation-status),
105
- [`PHASE5.md`](./docs/PHASE5.md#00-implementation-status). If this table and one
106
- of those disagree, the phase document is right and this is stale.
107
-
108
- **CI runs again as of 2026-08-16**, after a gap since 2026-07-23 that ended
109
- when this repository was made public. A green check is evidence once more — of
110
- what the jobs cover. Gate locally with `just` first; CI is the second opinion.
111
-
112
189
  ## First five minutes, with no data at all
113
190
 
114
191
  The block above assumes you have a recording. This one assumes only the clone:
@@ -134,7 +211,44 @@ so the printed translation is their interpolated midpoint, and `plan()` is the
134
211
  object you keep — compiling the route once and evaluating it many times is the
135
212
  whole shape of the fast path.
136
213
 
137
- Two things surprise people, both deliberate:
214
+ The same shape in Rust, which is where the engine actually lives. This block is
215
+ compiled by `cargo test --doc`, so it cannot drift away from the API:
216
+
217
+ ```rust
218
+ use tf_tree::{Capacity, EdgeCfg, Iso3, Quat, Stamp, TreeBuilder, Vec3};
219
+
220
+ // Topology is declared up front: `build()` sizes one flat arena from exactly
221
+ // these edges, and nothing allocates after it returns.
222
+ let tree = TreeBuilder::new()
223
+ .static_edge("base_link", "lidar_top", &Iso3::IDENTITY) // (parent, child)
224
+ .dynamic_edge("odom", "base_link", EdgeCfg::new(Capacity::history(100.0, 10.0)))
225
+ .build()
226
+ .expect("layout");
227
+
228
+ let odom = tree.frame("odom").expect("declared");
229
+ let base_link = tree.frame("base_link").expect("declared");
230
+ let lidar_top = tree.frame("lidar_top").expect("declared");
231
+
232
+ // One writer per edge, enforced by the claim table rather than by convention.
233
+ // Note the order flips: the builder takes (parent, child), `claim` takes
234
+ // (child, parent). Both are annotated here because getting it wrong builds a
235
+ // silently inverted tree rather than failing.
236
+ let w = tree.claim(base_link, odom).expect("unclaimed"); // (child, parent)
237
+ let at_x = |x| Iso3::new(Quat::IDENTITY, Vec3::new(x, 0.0, 0.0));
238
+ w.push(1_000_000_000, &at_x(0.0)).expect("monotonic"); // integer nanoseconds
239
+ w.push(1_010_000_000, &at_x(1.0)).expect("monotonic");
240
+
241
+ // Compile the route once, evaluate it many times.
242
+ let plan = tree.plan(odom, lidar_top).expect("connected");
243
+ let g = tree.guard();
244
+ let t: Stamp = Stamp::from_nanos(1_005_000_000);
245
+ let pose = plan.at(&g, t).expect("in range");
246
+ assert!((pose.t.x - 0.5).abs() < 1e-12);
247
+ ```
248
+
249
+ [`crates/tf_tree/README.md`](./crates/tf_tree/README.md) carries the annotated
250
+ version, including what a failed lookup prints. Two things surprise people about
251
+ both languages, and both are deliberate:
138
252
 
139
253
  - **Stamps are integer nanoseconds.** There is no float-seconds overload. At a
140
254
  2026 epoch the ULP of `float64` seconds is 238 ns, so every interval in a
@@ -144,11 +258,90 @@ Two things surprise people, both deliberate:
144
258
  copy" here means no *intermediate* allocation — use `Plan.at_into` to supply
145
259
  the destination.
146
260
 
147
- Rust is `cargo add tf_tree`; Python is `pip install transform_tree` — the
148
- distribution name differs from the import name, because PyPI refuses `tf_tree`
149
- as too close to the existing `tftree` ([`0008`](./docs/decisions/0008-the-name-tf-tree.md)
150
- records the measurement). `import tf_tree` either way. `just` alone lists
151
- everything the repository can do.
261
+ `just` alone lists everything the repository can do.
262
+
263
+ ## Where it fits, and where it does not
264
+
265
+ The fastest way to evaluate this is to find yourself on one of these two lists.
266
+ The second one is not a roadmap: most of its rows are decisions, recorded, that
267
+ will not be reversed.
268
+
269
+ **Reach for `tf_tree` when:**
270
+
271
+ - You look up transforms **inside a loop that has a deadline** — a controller, a
272
+ perception front end — and the per-lookup cost is a thing you have measured.
273
+ - **Many readers share one host**: threads in a process, or several processes.
274
+ One arena serves all of them, read-only by default, with no middleware between
275
+ the reader and the bytes.
276
+ - You train on recorded data and want **transforms in the dataloader** without a
277
+ ROS node in the training loop, or a pickle of precomputed poses that can no
278
+ longer answer at an arbitrary time.
279
+ - Your edges are **fast**, kilohertz-class, and float-seconds stamps have
280
+ already cost you resolution.
281
+ - You need to **debug** a transform tree — typed errors that name the edge, the
282
+ `TFT001`–`TFT019` catalogue, `tf_tree top`, and `doctor` against a bag with
283
+ nothing deployed.
284
+
285
+ **Look elsewhere when:**
286
+
287
+ | You need | Why not this | Where it is written down |
288
+ |---|---|---|
289
+ | A drop-in `tf2_ros::Buffer` | Phase 7, gated on operating evidence, not scheduled. What exists is a one-way ingest bridge | [`PHASE7.md`](./docs/PHASE7.md) §0.0 |
290
+ | Covariance or joint uncertainty | A tree cannot compose a correct one; composing marginals as independent is wrong in the optimistic direction. You need a factor graph | [`PROJECT.md`](./docs/PROJECT.md) §1, [`0009`](./docs/decisions/0009-descoping-phase-6.md) |
291
+ | Multi-parent frames, loop closure, copy-on-write branches | Multi-parent is the row above. Copy-on-write was cut for reasons of its own: it serves the use case D2 rejects *and* contradicts fixed capacity, one-writer-per-edge and append-only ids at once | [`0009`](./docs/decisions/0009-descoping-phase-6.md), [`PROJECT.md`](./docs/PROJECT.md) §5 D2 |
292
+ | Transforms across hosts | Phase 8. Not started | [`PROJECT.md`](./docs/PROJECT.md) §4 |
293
+ | Shared memory or `.tft` off Linux | The engine compiles; the mapping code does not exist elsewhere | [`SUPPORT.md`](./SUPPORT.md) |
294
+ | A viewer, or point-cloud deskewing | Deliberately absent, argument recorded. `at_adaptive` emits knots; the consumer transforms points where they already live | [`PHASE5.md`](./docs/PHASE5.md) §8, [`PROJECT.md`](./docs/PROJECT.md) §5 D8 |
295
+ | An API that will not move under you | `0.0.x`: every release may break every other | [`CHANGELOG.md`](./CHANGELOG.md) |
296
+
297
+ ## Status
298
+
299
+ **On crates.io from 0.0.1; on PyPI from 0.0.2.** The five engine crates are
300
+ published — `cargo add tf_tree`. The Python wheel starts at 0.0.2 because the
301
+ 0.0.1 commit did not compile off Linux, so no wheel for it exists or can; see
302
+ [`CHANGELOG.md`](./CHANGELOG.md).
303
+
304
+ | Phase | What it is | Status |
305
+ |---|---|---|
306
+ | 1 | Single-process engine: arena, seqlock buffers, plans, SE(3) math | **Implemented** |
307
+ | 2 | Shared memory: rendezvous, fd passing, claims as leases, reaping | **Implemented**, with gaps |
308
+ | 3 | Python bindings (PyO3, zero intermediate allocation) | **Implemented** |
309
+ | 4 | C ABI, C++ wrapper, ROS 2 ingest bridge, derivatives | **Implemented**, with gaps |
310
+ | 5 | Frozen `.tft` arena, bag ingestion, diagnostics, `tf_tree top` | **Mostly implemented** |
311
+ | 6–8 | Continuous-time interpolation, `tf2` shim, multi-host replication | Not started |
312
+
313
+ The gaps, named — because "with gaps" on its own is not a status:
314
+
315
+ - **Phase 2** — the daemon and recorder surface (§9–§10) are absent, and
316
+ §11.3's fault injection is a separate gap still being worked. §3.5's ownership
317
+ migration **landed on 2026-08-28**: kill the arena's owner and a surviving
318
+ read-write participant inherits the role, so new processes can join again. Its
319
+ trigger is caller-driven — `Tree::owner_lost()` is a non-blocking check a
320
+ survivor makes in its own loop, and nothing makes it for you, because there is
321
+ no daemon.
322
+ - **Phase 4** — everything except §5.9's affinity knobs and §6.3's replay rows.
323
+ `at_with_derivatives`, both headers, the header-only C++ wrapper with its
324
+ CMake package, and **both halves of the ingest bridge** — the `rclcpp` package
325
+ in `ros/tf_tree_ros` included. §7's benchmark gate is partial, and §1's exit
326
+ criterion is **operational, not a feature list**: it is open, and no amount of
327
+ code closes it.
328
+ - **Phase 5** — `FORMAT_VERSION = 3`, the frozen arena (§2), the offline Python
329
+ API (§4), the §5 counters and `tf_tree top` (terminal *and* `--web`, §7) have
330
+ all landed. Ingestion is **MCAP only** (§3). The `TFT001`–`TFT019` catalogue
331
+ reports all nineteen ids, of which seventeen can detect (§6). §8 is
332
+ **deliberately not built**. §9's benchmark artifact and §10's release
333
+ readiness are partial.
334
+ - **Phase 7** is gated by D21 and none of its four gates is met.
335
+
336
+ **The per-phase `§0.0` tables in [`docs/`](./docs/) are the source of truth**, not
337
+ this one — [`PHASE2.md`](./docs/PHASE2.md#00-implementation-status),
338
+ [`PHASE4.md`](./docs/PHASE4.md#00-implementation-status),
339
+ [`PHASE5.md`](./docs/PHASE5.md#00-implementation-status). If this table and one
340
+ of those disagree, the phase document is right and this is stale.
341
+
342
+ **CI runs again as of 2026-08-16**, after a gap since 2026-07-23 that ended
343
+ when this repository was made public. A green check is evidence once more — of
344
+ what the jobs cover. Gate locally with `just` first; CI is the second opinion.
152
345
 
153
346
  ## Is this a `tf2` replacement?
154
347
 
@@ -223,7 +416,7 @@ defend against a hazard they do not have (`docs/PHASE5.md` §4.3).
223
416
 
224
417
  ## Workspace
225
418
 
226
- ```
419
+ ```text
227
420
  crates/
228
421
  ├── tf_tree_math/ no_std SE(3)/SO(3) + dual quaternions; #![forbid(unsafe_code)]
229
422
  ├── tf_tree_arena/ no_std+alloc pointer-free arena + layout math
@@ -292,17 +485,39 @@ regenerates the baseline; that diff belongs in the commit that causes it.
292
485
  Standing numbers and their caveats live in
293
486
  [`docs/benchmarks/`](./docs/benchmarks/).
294
487
 
295
- ## Reading order
488
+ ## Where to go next
489
+
490
+ **Using it.**
491
+
492
+ 1. [`docs/API.md`](./docs/API.md) — the cross-cutting contract: six rules (§1)
493
+ every binding obeys, and the normative Rust, Python, C and C++ surfaces
494
+ (§2–§5). This is the reference.
495
+ 2. [`docs/RUNBOOK.md`](./docs/RUNBOOK.md) — organised by **symptom**, because
496
+ that is what you have when a robot's transform tree misbehaves. Every row
497
+ names an error type and, where one exists, the `doctor` check that finds it.
498
+ 3. [`docs/benchmarks/`](./docs/benchmarks/) — the standing measurements, each
499
+ row naming the command that produced it.
500
+
501
+ **Changing it.**
296
502
 
297
503
  1. [`docs/PROJECT.md`](./docs/PROJECT.md) — overview, architecture, roadmap, and
298
- the decision log D1–D22 in §5.
299
- 2. [`docs/API.md`](./docs/API.md) — the cross-cutting contract: six rules (§1)
300
- every binding obeys, and the §7 checklist a new surface passes. It is not a
301
- phase and authorizes nothing on its own.
302
- 3. The phase spec you care about: [`PHASE1`](./docs/PHASE1.md) …
303
- [`PHASE5`](./docs/PHASE5.md). Each opens with its own status table.
304
- 4. [`docs/decisions/`](./docs/decisions/) — the records for things the phase
305
- specs do not cover.
504
+ the decision log D1–D22 in §5. Read it before proposing anything: several
505
+ obvious-looking simplifications are excluded on purpose, and the reasons are
506
+ there.
507
+ 2. The phase spec you are touching: [`PHASE1`](./docs/PHASE1.md) …
508
+ [`PHASE5`](./docs/PHASE5.md). `PHASE2`, `PHASE4`, `PHASE5` and `PHASE7` open
509
+ with a `§0.0` status table, and it outranks every other document including
510
+ this one. `PHASE1` has none because Phase 1 is implemented whole, and
511
+ `PHASE3` has none because it records deviations inline, in the section each
512
+ belongs to.
513
+ 3. [`docs/API.md`](./docs/API.md) again, and **before** writing any public
514
+ surface — §1's six rules generate every binding, and §7 is the checklist a
515
+ new surface passes. It authorizes nothing on its own: its §6 delta table
516
+ names the phase or decision record each row lands in.
517
+ 4. [`docs/decisions/`](./docs/decisions/) — the records for what the phase specs
518
+ do not cover, and where a change of that kind starts.
519
+ 5. [`CONTRIBUTING.md`](./CONTRIBUTING.md) — the local gates, and the order to
520
+ run them in.
306
521
 
307
522
  ## Contributing and support
308
523