Sample the environment layer field in the fluid models - #3461
Draft
teerthsharma wants to merge 2 commits into
Draft
Sample the environment layer field in the fluid models#3461teerthsharma wants to merge 2 commits into
teerthsharma wants to merge 2 commits into
Conversation
teerthsharma
force-pushed
the
feat/env-sampler
branch
from
August 5, 2026 09:08
c219bfd to
a839c8b
Compare
This was referenced Aug 5, 2026
teerthsharma
force-pushed
the
feat/env-sampler
branch
from
August 5, 2026 09:47
a839c8b to
c40c6d3
Compare
mjOption carries one gravity, wind, density and viscosity that hold everywhere in the model. This adds an ordered stack of horizontal medium slabs that let those four quantities vary with height along +z. mjModel gains nlayer and the layer_height, layer_gravity, layer_density, layer_viscosity and layer_wind arrays. mjSpec gains mjsLayer together with mjs_addLayer, mjs_getLayer and mjs_deleteLayers. <option> accepts any number of <layer> children, each inheriting its unspecified attributes from the global medium, and the compiler rejects heights that are not strictly ascending. Layers are unnamed global option data rather than model elements: they have no mjtObj type, no name and no id, hence no mjsElement, and the stack is cleared at once rather than deleted element by element. The compiler holds them in a deque so that pointers returned by mjs_addLayer survive later adds. Nothing reads the new fields yet. A model with no <layer> element compiles to nlayer == 0 and is unchanged. Signed-off-by: teerthsharma <teerths57@gmail.com>
mjModel carries the layer stack but nothing reads it. mj_inertiaBoxFluidModel and mj_ellipsoidFluidModel take wind, density and viscosity straight from mjOption, so a layered model still behaves as one global medium. This adds the reader and wires it into the two fluid models and their derivatives. mj_envLayer binary-searches the nlayer-1 interior boundaries for the layer containing a height, with a height exactly on a boundary opening the upper layer. mj_envSample fills an mjEnv with that layer's gravity, wind, density and viscosity. Both fluid models sample at the point the force is applied to, xipos for the inertia box and geom_xpos for the ellipsoid, and mjd_inertiaBoxFluid and mjd_ellipsoidFluid sample at the same points, so the derivative continues to match the force. mj_fluid previously early-returned on mjOption density and viscosity alone, which a layered model that leaves the global medium empty would wrongly take; mj_hasFluid asks the layers as well. The stack is stratified along +z rather than a general cell arrangement because a piecewise-constant gravity field has a potential only when every value jump is parallel to the normal of the face it crosses. Parallel layers satisfy that for any assignment of values; a vertical split does not, and a closed circuit across one gains energy without bound. A model with nlayer == 0 takes the single-medium path, in which mj_envSample copies mjOption bit for bit, so existing models are unaffected. Signed-off-by: teerthsharma <teerths57@gmail.com>
teerthsharma
force-pushed
the
feat/env-sampler
branch
from
August 5, 2026 11:02
c40c6d3 to
7d568e9
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Second of two steps toward #3452. #3460 adds the layer stack; this adds the reader and points the fluid models at it.
Relates to #3452. Depends on #3458 for sanitizer builds.
Stacked on #3460. Review that first; this branch contains only the commit on top of it.
Summary
#3460 adds the layer stack to
mjModelbut nothing reads it. This adds the reader and points the fluid models at it.mj_envLayerreturns the layer containing a height, andmj_envSamplefills anmjEnvwith the gravity, wind, density and viscosity in force at a world point.mj_ellipsoidFluidModelandmj_inertiaBoxFluidModelnow sample at each geom's own position instead of readingmjOption, andmjd_ellipsoidFluidandmjd_inertiaBoxFluidsample at the same points so force and derivative stay consistent.When
nlayer <= 1,mj_envSamplecopiesmjOptionverbatim, so single-medium models take the same values they always did.Gravity is deliberately not converted. It does not enter as a force at a point:
mj_rnesets the world frame's fictitious acceleration to-gand propagates it down the tree, so each body picks it up throughcinert * cacc, an integral over its mass distribution with no sample point to name. Three further reads are locked to that one —mj_gravcompmust cancel it exactly,mj_energyPosmust match it, and themj_fwdActuationgate — so sampling any one atxiposwould desynchronise the others for every body spanning a boundary. Making gravity a field needs the integral, not a sample, and is left out.mj_fluidandmjd_passive_veleach early-returned onmjOptionalone, so a stratified model withopt.density == 0would have received no fluid force at all. Both now gate onmj_hasFluid, which also consults the layers. Atnlayer == 0the layer loop does not execute and the test is identical to the old one.Determinism
mj_envLayerperforms no floating-point arithmetic: it reads heights, compares, and shifts integers. There is no add or multiply onmjtNum, so nothing is exposed to FMA contraction, x87 excess precision or vectorized reassociation, and the result is bit-identical across compilers, platforms and optimization levels. Index arithmetic is done inmjtSizethroughout, so nothing is narrowed. The loop runs exactlyceil(log2(nlayer-1))times, with no data-dependent iteration count, no global state and no warm start. A height exactly on a boundary belongs to the upper layer;+infselects the top layer;-infand NaN select layer 0, because every comparison against NaN is false and the search then always takes the upper branch.Validation
EnvironmentTestunder ASan + UBSan + LeakSanitizer, clang 21.1.8The sanitizer run is only meaningful with a positive control, so: the test binary carries 224
__asan/__ubsansymbols, and a deliberately plantedheap-buffer-overflowcompiled with the same flags is caught and reported. Building it required #3458 applied locally,-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFFto avoid a ThinLTO link failure, andCMAKE_*_FLAGS_INITrather thanCMAKE_*_FLAGSso CMake's own defaults survive.The 14 tests cover the boundary tie convention at ULP resolution, binary search against a linear scan over 64 models with 1 to 64 layers and 39 queries each, bit-identical
mjOptionfallback atnlayer0 and 1 with poison values in the layer arrays, force and derivative each following the field, and seven adversarial cases: non-finite query heights, index always in range, monotonicity on adjacent points, statelessness across interleaved calls,-0.0and denormal and 1-ulp-apart boundaries, single-medium purity, and idempotent sampling.Test quality was measured by mutation rather than asserted. Eight mutations of the implementation were applied and reverted:
layer_height[mid] <= zto< znlayer <= 1early returnhi = nboundtohi = nbound + 1!(z < h[mid])(z, layer)across callsenv->viscosityon the layered pathNo test survives every mutation. Two mutations of an earlier revision survived, and investigating why showed the code they targeted was unreachable: a clamp on
nlayermade the midpoint overflow it guarded against impossible, while itself converting a narrowing conversion into an out-of-bounds read. The clamp is gone and the search runs inmjtSize.Cost
Measured by calling
mj_envSampleover 256 real geom positions, 2000 rounds per batch, best of 11, pinned at real-time priority:nlayer+0.88 ns per search iteration, matching
O(log nlayer). One sample per geom per step underEuler, two underimplicitandimplicitfast. On a 256-geom fluid scene that predicts +0.6% per step, which is below what step-level timing can resolve on this host; the step-level measurement of the same scenes indeed returns values inside its own noise, including physically impossible negative ones.Limits
Gravity remains global, as above.
mjOptionvalues are still read by everything outside the two fluid models. The stratification is horizontal slabs along +z only.The single-medium claim is exercised by the shipped corpus, where every model has
nlayer == 0, and by the bit-identical trajectory checksums reported on #3460; the layered path is exercised only by the new tests and by synthetic scenes.Sanitizer coverage is the
EnvironmentTestbinary, not the full suite, which did not complete under sanitizers on this host within a reasonable time. MJX and Warp are not extended.