feat(models): H-Net and diffusion model zoo - #554
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
38feb8d to
248369c
Compare
f005a7b to
31db2c8
Compare
Deletions -- each has zero importers on main, verified by module-path search,
per-symbol search, hydra _target_ strings, `python -m` invocations, file-path
references in shell/CI, and dynamic-import patterns:
egomimic/utils/memory_utils.py 229
egomimic/utils/real_utils.py 142
egomimic/pl_utils/test_model_wrapper.py 135
egomimic/rldb/compression_utils.py 130
egomimic/rldb/data_utils.py 103
egomimic/rldb/scripts/utils.py 86
egomimic/utils/obs_utils.py 10
Two worth calling out:
* rldb/scripts/utils.py is a dead DUPLICATE -- all five of its functions
(nds_pq, is_key, is_listy, nds, str2bool) also exist in
egomimic/utils/egomimicUtils.py, and every caller imports them from there.
* compression_utils.py is the superseded video codec (encode_video /
decode_video on PyAV). Its replacement is rldb/zarr/video_codec.py, which
arrives with the h264 PR. Nothing on main imports compression_utils. The
only importer anywhere is rldb/zarr/verify_zarr.py, which exists solely on
six branches last touched in February 2026 and on neither main nor the
batchflow stack; those branches carry their own copy of that file and would
need a rebase regardless.
Rename:
egomimic/rldb/zarr/zarr_write_test.py -> egomimic/rldb/zarr/hdf5_to_zarr.py
The name said "test", but it is not one: it is an HDF5 -> Zarr v3 conversion
utility with an argparse CLI (--hdf5-path / --arm / --extrinsics-key), forward
kinematics and camera-frame transforms. The new name matches the existing
aria_to_zarr / eva_to_zarr / mecka_to_zarr convention. Nothing referenced the
old name.
Deliberately KEPT, having been checked rather than assumed:
* benchmark_forward_pass.py -- a working dataloader-throughput benchmark with
an argparse CLI and no hardcoded paths.
* test_zarr.py -- a genuine pytest module (13 tests over MultiDataset /
ZarrDataset / action-chunk transforms). It is currently uncollected because
of five hardcoded absolute paths and an import of
build_human_bimanual_transform_list, which exists on no branch. That makes
it a broken test to repair, not dead code to delete.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deletions -- each has zero importers on main, verified by module-path search,
per-symbol search, hydra _target_ strings, `python -m` invocations, file-path
references in shell/CI, and dynamic-import patterns:
egomimic/utils/memory_utils.py 229
egomimic/utils/real_utils.py 142
egomimic/pl_utils/test_model_wrapper.py 135
egomimic/rldb/compression_utils.py 130
egomimic/rldb/data_utils.py 103
egomimic/rldb/scripts/utils.py 86
egomimic/utils/obs_utils.py 10
Two worth calling out:
* rldb/scripts/utils.py is a dead DUPLICATE -- all five of its functions
(nds_pq, is_key, is_listy, nds, str2bool) also exist in
egomimic/utils/egomimicUtils.py, and every caller imports them from there.
* compression_utils.py is the superseded video codec (encode_video /
decode_video on PyAV). Its replacement is rldb/zarr/video_codec.py, which
arrives with the h264 PR. Nothing on main imports compression_utils. The
only importer anywhere is rldb/zarr/verify_zarr.py, which exists solely on
six branches last touched in February 2026 and on neither main nor the
batchflow stack; those branches carry their own copy of that file and would
need a rebase regardless.
Rename:
egomimic/rldb/zarr/zarr_write_test.py -> egomimic/rldb/zarr/hdf5_to_zarr.py
The name said "test", but it is not one: it is an HDF5 -> Zarr v3 conversion
utility with an argparse CLI (--hdf5-path / --arm / --extrinsics-key), forward
kinematics and camera-frame transforms. The new name matches the existing
aria_to_zarr / eva_to_zarr / mecka_to_zarr convention. Nothing referenced the
old name.
Deliberately KEPT, having been checked rather than assumed:
* benchmark_forward_pass.py -- a working dataloader-throughput benchmark with
an argparse CLI and no hardcoded paths.
* test_zarr.py -- a genuine pytest module (13 tests over MultiDataset /
ZarrDataset / action-chunk transforms). It is currently uncollected because
of five hardcoded absolute paths and an import of
build_human_bimanual_transform_list, which exists on no branch. That makes
it a broken test to repair, not dead code to delete.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
e477a60 to
5be23a7
Compare
Claude Code ReviewReviewSummaryMassive relocation + new-subsystem PR that vendors H-Net (stages, routing, blocks, scan), a diffusion model zoo (DiT3D, spatial backbones, VAE, samplers), and reorganizes cores/stems. Roughly 15k+ lines added across new files, plus a heavily rewritten Key concerns1. PR scope is dangerously largeThis bundles at least four independent efforts:
Any of these could break existing training configs. Splitting into ≥3 PRs would make review tractable and let bisect actually work. Strongly recommend splitting before merge. 2.
|
…odules
egomimicUtils.py was a 1,120-line grab-bag. Rather than inventing new modules,
the 20 symbols that still have callers move into the modules that already
exist and already own that concern:
pose_utils.py +9 pose/transform/camera-frame + the two array interpolators
viz_utils.py +3 draw_actions, draw_dot_on_frame, get_gaze_endpoint
tensor_utils.py +2 get_sinusoid_encoding_table, EinOpsRearrange
utils.py +4 str2bool, fmt, download_from_huggingface, STD_SCALE
metrics.py +2 frechet_gaussian_over_time, reverse_kl_from_samples (NEW --
the only case with no existing home; both are eval metrics
consumed solely by eval/eval_hpt.py)
viz_utils already imported draw_actions/draw_dot_on_frame/get_gaze_endpoint from
egomimicUtils, so moving them there removes an import rather than adding one.
Code is moved verbatim (exact AST source spans). egomimicUtils.py re-exports the
moved symbols, so all existing importers keep working unchanged.
Imports are resolved, not copied: each destination gets exactly the third-party
imports its moved code references, cross-module references point at the real new
home, and any destination that previously imported a now-local symbol from
egomimicUtils has that import removed -- otherwise egomimicUtils re-exporting from
viz_utils while viz_utils imports from egomimicUtils is a circular import.
The 24 symbols with no caller anywhere are left in egomimicUtils untouched;
removing them is a separate decision and a separate commit.
Verified: ruff F821/F811 clean on egomimic/utils; all 20 moved symbols resolve
through the old path; zero unresolvable global references in any moved function
body (checked by walking each bodys AST against its new module globals -- the
check that a plain import test does not catch); 13/14 importer modules import,
the 14th being egomimic.robot.rollout which fails on a pre-existing unrelated
bare robot_utils import.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rollout.py and collect_demo.py did:
from robot_utils import RateLoop
but there is no top-level robot_utils package -- the file is
egomimic/robot/robot_utils.py. That bare form only resolves when the script is
run from inside egomimic/robot/ (that directory on sys.path), so importing
either module as part of the package raised ModuleNotFoundError. It is also why
rollout.py looked dead to reference sweeps: it is the sole caller of
egomimicUtils.interpolate_arr / interpolate_arr_euler, and an import-based check
never got far enough to see it.
stream_aria.py and stream_d405.py already use the package-qualified form; this
makes all four consistent.
Verified: egomimic.robot.rollout now imports (previously ModuleNotFoundError).
collect_demo still fails on ppadb, an unrelated optional hardware dependency for
the Oculus reader that is simply absent from the cluster venv.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rollout.py and collect_demo.py did:
from robot_utils import RateLoop
but there is no top-level robot_utils package -- the file is
egomimic/robot/robot_utils.py. That bare form only resolves when the script is
run from inside egomimic/robot/ (that directory on sys.path), so importing
either module as part of the package raised ModuleNotFoundError. It is also why
rollout.py looked dead to reference sweeps: it is the sole caller of
egomimicUtils.interpolate_arr / interpolate_arr_euler, and an import-based check
never got far enough to see it.
stream_aria.py and stream_d405.py already use the package-qualified form; this
makes all four consistent.
Verified: egomimic.robot.rollout now imports (previously ModuleNotFoundError).
collect_demo still fails on ppadb, an unrelated optional hardware dependency for
the Oculus reader that is simply absent from the cluster venv.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Completes the breakup: the compatibility shim is gone and the module no longer exists. The 9 files that imported from it now import from the module that actually owns each symbol -- pose_utils, viz_utils, tensor_utils, utils, metrics. Each import statement was rewritten by mapping every imported name through the shim's own re-export table, so aliases are preserved and nothing is guessed: algo/hpt.py models/hpt_nets.py eval/eval_act.py robot/rollout.py eval/eval_hpt.py scripts/data_visualization.py scripts/aria_process/aria_to_zarr.py scripts/eva_process/eva_to_zarr.py scripts/data_visualization/inspector_lib/dataset_view.py The 24 symbols that had no home and no caller are removed with the file. Each was checked individually for any reference in .py, .yaml or .ipynb outside egomimicUtils itself -- all 24 came back with zero. This is the check that previously caught get_vector_from_yaw_pitch, which had no external references but was called by get_gaze_endpoint, and so was moved rather than dropped. Verified: no code reference to egomimicUtils remains anywhere (only three explanatory comments naming the old module); ruff F821/F811 clean across egomimic/*.py; 13/14 affected modules import. The 14th, egomimic.robot.rollout, fails on the unrelated bare robot_utils import fixed separately in #563. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit parked str2bool / fmt / STD_SCALE / download_from_huggingface
in utils/utils.py. That was wrong: utils.py is the hydra + lightning experiment
lifecycle module (extras, task_wrapper, get_metric_value), so this just traded one
grab-bag for another.
str2bool -> type_utils.py (a str->bool coercion, next to the
existing _to_numpy coercion)
download_from_huggingface -> hf_utils.py (new; keeps the Hub dependency
visible and optional, mirroring the aws/ subpackage)
STD_SCALE -> algo/hpt.py (not a shared utility at all -- it is
the HPT action-token init scale with exactly one
consumer, 20 lines below where it now lives)
fmt -> deleted
fmt turned out to be dead. It was classified as live off a reference count that
word-matched unrelated keyword arguments in plot_cartesian.py and in
vendored lerobot. Nothing has ever imported it -- not on main, not now.
utils.py is back to its three lifecycle helpers.
Verified: ruff F401/F821/F811 clean on egomimic/utils and algo/hpt.py; 13/14
affected modules import (the 14th is robot/rollout, pending #563).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
chore: remove seven unused modules, rename a mis-named converter
Deletions -- each has zero importers on main, verified by module-path search,
per-symbol search, hydra _target_ strings, `python -m` invocations, file-path
references in shell/CI, and dynamic-import patterns:
egomimic/utils/memory_utils.py 229
egomimic/utils/real_utils.py 142
egomimic/pl_utils/test_model_wrapper.py 135
egomimic/rldb/compression_utils.py 130
egomimic/rldb/data_utils.py 103
egomimic/rldb/scripts/utils.py 86
egomimic/utils/obs_utils.py 10
Two worth calling out:
* rldb/scripts/utils.py is a dead DUPLICATE -- all five of its functions
(nds_pq, is_key, is_listy, nds, str2bool) also exist in
egomimic/utils/egomimicUtils.py, and every caller imports them from there.
* compression_utils.py is the superseded video codec (encode_video /
decode_video on PyAV). Its replacement is rldb/zarr/video_codec.py, which
arrives with the h264 PR. Nothing on main imports compression_utils. The
only importer anywhere is rldb/zarr/verify_zarr.py, which exists solely on
six branches last touched in February 2026 and on neither main nor the
batchflow stack; those branches carry their own copy of that file and would
need a rebase regardless.
Rename:
egomimic/rldb/zarr/zarr_write_test.py -> egomimic/rldb/zarr/hdf5_to_zarr.py
The name said "test", but it is not one: it is an HDF5 -> Zarr v3 conversion
utility with an argparse CLI (--hdf5-path / --arm / --extrinsics-key), forward
kinematics and camera-frame transforms. The new name matches the existing
aria_to_zarr / eva_to_zarr / mecka_to_zarr convention. Nothing referenced the
old name.
Deliberately KEPT, having been checked rather than assumed:
* benchmark_forward_pass.py -- a working dataloader-throughput benchmark with
an argparse CLI and no hardcoded paths.
* test_zarr.py -- a genuine pytest module (13 tests over MultiDataset /
ZarrDataset / action-chunk transforms). It is currently uncollected because
of five hardcoded absolute paths and an import of
build_human_bimanual_transform_list, which exists on no branch. That makes
it a broken test to repair, not dead code to delete.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
chore: remove seven unused modules, rename a mis-named converter (#561)
Deletions -- each has zero importers on main, verified by module-path search,
per-symbol search, hydra _target_ strings, `python -m` invocations, file-path
references in shell/CI, and dynamic-import patterns:
egomimic/utils/memory_utils.py 229
egomimic/utils/real_utils.py 142
egomimic/pl_utils/test_model_wrapper.py 135
egomimic/rldb/compression_utils.py 130
egomimic/rldb/data_utils.py 103
egomimic/rldb/scripts/utils.py 86
egomimic/utils/obs_utils.py 10
Two worth calling out:
* rldb/scripts/utils.py is a dead DUPLICATE -- all five of its functions
(nds_pq, is_key, is_listy, nds, str2bool) also exist in
egomimic/utils/egomimicUtils.py, and every caller imports them from there.
* compression_utils.py is the superseded video codec (encode_video /
decode_video on PyAV). Its replacement is rldb/zarr/video_codec.py, which
arrives with the h264 PR. Nothing on main imports compression_utils. The
only importer anywhere is rldb/zarr/verify_zarr.py, which exists solely on
six branches last touched in February 2026 and on neither main nor the
batchflow stack; those branches carry their own copy of that file and would
need a rebase regardless.
Rename:
egomimic/rldb/zarr/zarr_write_test.py -> egomimic/rldb/zarr/hdf5_to_zarr.py
The name said "test", but it is not one: it is an HDF5 -> Zarr v3 conversion
utility with an argparse CLI (--hdf5-path / --arm / --extrinsics-key), forward
kinematics and camera-frame transforms. The new name matches the existing
aria_to_zarr / eva_to_zarr / mecka_to_zarr convention. Nothing referenced the
old name.
Deliberately KEPT, having been checked rather than assumed:
* benchmark_forward_pass.py -- a working dataloader-throughput benchmark with
an argparse CLI and no hardcoded paths.
* test_zarr.py -- a genuine pytest module (13 tests over MultiDataset /
ZarrDataset / action-chunk transforms). It is currently uncollected because
of five hardcoded absolute paths and an import of
build_human_bimanual_transform_list, which exists on no branch. That makes
it a broken test to repair, not dead code to delete.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
refactor(utils): break egomimicUtils apart into role-based modules
egomimicUtils.py had grown to 1,120 lines holding 44 unrelated symbols --
camera geometry, frame drawing, interpolation, distribution metrics, torch
building blocks and assorted helpers all in one file. It is the kind of module
everything imports and nobody can safely change.
Its contents now live in six modules next to it:
utils/geometry.py 18 symbols, 397 lines pose/transform/camera-frame
utils/viz_draw.py 8 symbols, 273 lines frame annotation + drawing
utils/interpolation.py 3 symbols, 100 lines array / euler interpolation
utils/metrics.py 2 symbols, 100 lines Frechet, reverse KL
utils/nn_utils.py 2 symbols, 28 lines sinusoid table, EinOpsRearrange
utils/misc.py 11 symbols, 108 lines str2bool, fmt, download, ...
Code is moved verbatim -- each symbol's exact source span was extracted by AST,
so nothing was reformatted or rewritten. egomimicUtils.py becomes a 109-line
re-export shim, so all ten existing importers keep working with no change and
`from egomimic.utils.egomimicUtils import X` still resolves for every X. New
code should import from the specific module.
NOTHING IS DELETED here. Twenty-five of the 44 symbols have no reference
anywhere outside the file (~533 lines) -- draw_rotation_text 89, ypr_to_matrix
53, draw_annotation_text 38, convert_to_cam_frame 35, miniviewer 32, nds 29,
nds_pq 25, AlohaFK 24, apply_vignette 14, and others. Removing them is a
separate judgement call and belongs in its own commit: several are plausible
hand-run tooling rather than dead weight, and "no importer" has already proved
a poor proxy for "unwanted" in this repo.
Verified: all 44 symbols re-export from the old path; every symbol is the SAME
object as in its new home (no accidental copies); all six modules import; all
ten importing files still parse.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The reconciliation layer. Every file where main and the batchflow lineage genuinely disagreed lands here, so everything above it is bulk-new code. embodiment.py takes main's collapsed enum (HUMAN_* 1-3, EVA_* 4-6) and re-adds PUSHSHAPES_SIM 15 / _STICK 16 / _SMALL_CIRCLE 17 -- pinned because trained checkpoints and collected datasets encode those IDs. zarr_dataset_multi.py is a 3-way merge against the fork point: main's SafeS3EpisodeResolver, EvenStrideDataset, _evenly_spaced_indices and intrinsics property, plus batchflow's _read_span, _annotations_for_span and LocalEpisodeResolverWithEmbodimentOverride. action_chunk_transforms.py keeps batchflow's DeltaAction alongside main's PadGripperZeros. The batchflow repo notes land in AGENTS.md rather than a second CLAUDE.md, so the repo keeps one conventions file. DESIGN.md is not carried over: it was a 2026-06-06 restructure proposal written against EgoVerse-pact-2, still marked "awaiting approval", describing a move that has since happened here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The JPEG-decodability probe at _probe_image_key calls simplejpeg.decode_jpeg(...) but the module was never imported in this file. The call sits inside a try/except Exception, so instead of crashing it made the probe report EVERY image as undecodable -- a silent false negative rather than an error. _common.py in the same package already imports it the same way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AGENTS.md still documented egomimic/models/hnet_nets/, egomimic/algo/hnet.py and egomimic/eval/eval_hnet.py. None of those paths exist: the packages are models/hnet/, algo/hnet/ and eval/core/eval_hnet.py. Anyone -- human or agent -- following the doc went looking for files that are not there, and AGENTS.md is the first thing an agent reads. Section headings renamed hnet_nets -> hnet to match. test_hnet_nets.py is left alone: that file genuinely still has that name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…oader
This PR introduces the packed subsystem -- ZarrEpisodePackedDataset and
pack_collate -- but MultiDataModuleWrapper, which is inherited unchanged from
main, hardcoded annotation_collate for every dataset. annotation_collate ends in
default_collate, which tries to torch.stack ragged packed samples, so every
packed_episode config died on its first batch with
RuntimeError: Trying to resize storage that is not resizable
pack_collate needs two call sites. The other one -- MultiDataset's norm-stat
inference in zarr_dataset_multi.py -- already had it (collate_fn = pack_collate
if is_packed else None) and is covered by test_packed_pipeline. The training
dataloader had neither the wiring nor a test, and main has no packed configs at
all, so nothing exercised it.
_collate_fn_for is ported from EgoVerse-gmm-dualstream / EgoVerse2, where this
dispatch already backs the live H-Net runs, rather than written fresh.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two standalone simulator packages, each with its own pushshapes/, collect/, examples/ and tests/. There is no v3: what was labelled v3 is the socket fix that ships AS v2 -- the intermediate all-faces-grip build was a bug. __init__.py aliases the active version's submodules to the top level so existing 'from Tsimulation.pushshapes import X' call sites keep working; TSIM_VERSION selects the version per process. Placed before eval because eval calls into it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hing on them
Adding the u_socket was done by editing the environment. Its latch, friction and
penetration guards -- 12 methods, ~470 lines -- went into env.py as
`if pusher_shape == "u_socket"` branches, and its 3-DOF action became a hardcoded
`expected_shape = (3,) if self.pusher_shape == "u_socket" else (2,)`. That is the
single largest reason the two sims diverged: sim_v1's env.py has ZERO socket
references, sim_v2's had 115, and env.py's step loop called nine socket-specific
guards in sequence.
An Agent now owns the three things the environment should not know about:
* ACTION SPACE -- action_dim (2 for a free-moving pusher, 3 when the agent
also controls orientation) and target_pose() to decode a raw action;
* BODY -- build() in the pymunk space;
* CONTACT MODEL -- pre_substep()/post_substep() hooks around each physics
substep, plus on_reset() for per-episode state.
env.step is agent-agnostic:
captured = self.agent.pre_substep(self)
self._drive_pusher_toward(tx, ty, dt_sub, target_angle)
self._space.step(dt_sub)
self._clamp_pusher_to_static()
self.agent.post_substep(self, captured)
Agent (circle, circle_small, stick, L) implements the hooks as no-ops.
USocketAgent owns all the latch/guard logic and the socket geometry constants,
and its solid_pusher / socket_inside_friction_only flags become constructor
arguments rather than environment state. A new agent with an unusual action
space is a new class plus one line in make_agent(), not another branch in the
simulator.
env.py 1292 -> 795 lines.
sim_v1 IS DELIBERATELY UNTOUCHED. It is frozen so pre-rewrite data replays
exactly; refactoring it would put that at risk for no benefit, since it has no
socket to abstract in the first place.
VERIFIED BY REPLAY EQUIVALENCE, not by inspection. Baselined the unmodified sim
with the identical harness first, then compared:
u_socket_3000_v2 100.0% -> 100.0% (p50 0.0033 -> 0.0033)
circle_3000_plus_gen_v2 89.7% -> 89.7%
circle_v2_obstonly 17.1% -> 25.7%
The gate caught two real bugs that inspection did not: the moved
_socket_contact_is_on_inner_face call site lost its env argument, and
socket_latched -- a property DERIVED from `_socket_constraints is not None` --
had become a plain attribute nothing updated, initialised to [] so it would have
read as permanently latched. Both fixed; the socket went 0% -> 100%.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012V58H37tmcvgDthELMd5Xk
New subsystems with no counterpart on main: models/hnet (stages, blocks, the scan and register chunk interfaces, routing) and models/diffusion (DiT3D and spatial backbones, sampling, image VAE), plus cores and stems. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
widths=[512] in hpt_heads/hpt_stems and down_dims=[256,512,1024] in denoising_nets are evaluated once at import, so every caller that omits the argument shares one list object. None of the three currently mutates it, so nothing is broken today -- this removes the footgun before something does. Each becomes None with the original value restored inside the function, so the behaviour for an omitted argument is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
obs_encoder.py and cond_encoders.py each carried a private _mlp() of the same shape -- but ObsEncoder's used ReLU and CondEncoderModule's used GELU. Both are live: ObsEncoder backs nine bc_rnn model configs (base/tx/tx_chunk8/hnet/ tx_cotrain_*), CondEncoderModule backs the H-Net firstend/cotrain/bf_rh configs. They collapse into one build_mlp() whose 'act' argument is keyword-only with no default, so every call site states its activation. That is the point of the change: activations hold no parameters, so a checkpoint trained under one activation loads into the other with no error and merely produces different numbers -- exactly the kind of silent divergence two near-identical private helpers invite. Behaviour is unchanged: ReLU stays at both ObsEncoder call sites and GELU at both CondEncoderModule ones. Verified by building both encoders from the old and new trees -- identical activation lists, identical parameter keys, identical forward output hashes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

New subsystems with no counterpart on main: models/hnet (stages, blocks, the
scan and register chunk interfaces, routing) and models/diffusion (DiT3D and
spatial backbones, sampling, image VAE), plus cores and stems.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com