granulaire 0.2.0b3__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Daniel Régis Sarmento Caon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,541 @@
1
+ Metadata-Version: 2.4
2
+ Name: granulaire
3
+ Version: 0.2.0b3
4
+ Summary: Realistic granular assets generator for Physical AI: procedural grains with measured shape, mass and moisture, exported to OpenUSD for robot simulation and synthetic perception data.
5
+ Author: Daniel Régis Sarmento Caon
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/drscaon/granulaire
8
+ Project-URL: Issues, https://github.com/drscaon/granulaire/issues
9
+ Project-URL: Changelog, https://github.com/drscaon/granulaire/blob/main/CHANGELOG.md
10
+ Project-URL: Suite, https://github.com/drscaon/primvaria
11
+ Keywords: granular,simulation,openusd,procedural,robotics,physical-ai,synthetic-data,robot-learning,primvaria
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
16
+ Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: numpy>=1.24
21
+ Requires-Dist: scipy>=1.10
22
+ Requires-Dist: scikit-image>=0.21
23
+ Requires-Dist: trimesh>=4.0
24
+ Requires-Dist: usd-core>=24.0
25
+ Provides-Extra: render
26
+ Requires-Dist: matplotlib>=3.7; extra == "render"
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7; extra == "dev"
29
+ Requires-Dist: ruff>=0.5; extra == "dev"
30
+ Requires-Dist: matplotlib>=3.7; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ <h1 align="center">
34
+ <picture>
35
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/drscaon/granulaire/main/docs/images/logo-dark.svg">
36
+ <img alt="Granulaire" src="https://raw.githubusercontent.com/drscaon/granulaire/main/docs/images/logo.svg" width="420">
37
+ </picture>
38
+ </h1>
39
+
40
+ <p align="center"><strong>Realistic granular assets generator for Physical AI.</strong></p>
41
+
42
+ <p align="center">
43
+ <a href="https://github.com/drscaon/granulaire/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/drscaon/granulaire/actions/workflows/ci.yml/badge.svg"></a>
44
+ <img alt="Python 3.10+" src="https://img.shields.io/badge/python-3.10%2B-3776AB">
45
+ <img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-green">
46
+ </p>
47
+
48
+ Granulaire generates sand, gravel, pebbles, seeds, fruit, pellets, rubber mulch and packing peanuts
49
+ with realistic, measured shapes, sizes, masses and moisture, and exports them
50
+ to **OpenUSD**. It is built for **Physical AI**: robots trained and tested in
51
+ simulation (reinforcement learning, virtual test campaigns, sim-to-real) need
52
+ ground that looks and behaves like the real thing, not perfect spheres, and
53
+ perception models need synthetic images with exact ground truth. Everything
54
+ can run on CPU; see [Physics limits by grain size](#physics-limits-by-grain-size)
55
+ for what current engines can simulate.
56
+
57
+ ![Five grains of each preset in the four moisture states](https://raw.githubusercontent.com/drscaon/granulaire/main/docs/images/moisture_gallery.png)
58
+
59
+ *Five different grains of each of the 25 presets, dry, damp, wet and
60
+ saturated, rendered from the generated meshes. Each grain is drawn at its own
61
+ scale: sizes range from 0.07 mm (lunar regolith) to ~5 cm (river pebbles).
62
+ Regenerate with `granulaire render-gallery` (see Usage).*
63
+
64
+ ## What it does
65
+
66
+ - Generates *N* distinct grain shapes (100 by default) from distributions
67
+ borrowed from sedimentology: size on the phi scale (Wentworth), Zingg ratios
68
+ (elongation I/L, flatness S/I), angularity, irregularity (fracture faces,
69
+ lumps, pits) and mineralogy. Kernels, seeds and pellets use their own shape
70
+ families (superellipsoids, cylinders).
71
+ - Places each shape several times in shuffled order with random orientations:
72
+ by default 100 shapes × 5 copies = 500 grains per scene (see
73
+ [Scene size and mixing](#scene-size-and-mixing)).
74
+ - **Measures** every generated grain (nominal diameter, Zingg ratios, Wadell
75
+ sphericity, convexity, angularity proxy) instead of trusting the inputs.
76
+ - Exports an OpenUSD scene: prototypes, grains by reference, UsdPreviewSurface
77
+ materials, `UsdPhysics` rigid bodies (mass from volume and density,
78
+ convex-hull collision, friction, restitution) and a collision ground.
79
+ - Optionally gives every grain a moisture state through an OpenUSD variant set.
80
+ - Exports packing peanuts and cherry tomatoes as deformable bodies.
81
+ **Deformable simulation works only with NVIDIA OmniPhysics** (Omniverse,
82
+ Isaac Sim 5+) and is **not tested yet**; see
83
+ [Deformable grains](#deformable-grains-packing-peanuts-cherry-tomatoes).
84
+
85
+ ## Installation
86
+
87
+ From PyPI:
88
+
89
+ ```bash
90
+ pip install "granulaire[render]"
91
+ ```
92
+
93
+ From a clone, for development:
94
+
95
+ ```bash
96
+ python -m venv .venv && source .venv/bin/activate
97
+ pip install -e ".[render]" # add ",dev" for tests and linting
98
+ ```
99
+
100
+ Requires Python 3.10+.
101
+
102
+ ## Usage
103
+
104
+ ```bash
105
+ granulaire list
106
+ granulaire generate --preset beach_sand # 100 shapes × 5 copies, shuffled
107
+ granulaire generate --preset beach_sand --shapes 20 --copies 10 --no-shuffle --seed 3
108
+ granulaire generate --preset beach_pebbles --moisture --moisture-weights 0,20,50,30
109
+ granulaire generate --preset rounded_gravel --no-size --no-orientation
110
+ granulaire render output/beach_sand output/crushed_gravel
111
+ granulaire render-moisture output/beach_pebbles output/crushed_gravel output/beach_sand
112
+ granulaire render-gallery output/beach_sand output/crushed_gravel --grains 5
113
+ ```
114
+
115
+ The README gallery comes from generating every preset with `--moisture`, then
116
+ `granulaire render-gallery` on all 25 output folders in category order, with
117
+ `--output docs/images/moisture_gallery.png`.
118
+
119
+ `python -m granulaire ...` works too. From Python:
120
+
121
+ ```python
122
+ from granulaire import Variations, generate_scene, write_outputs
123
+
124
+ # defaults: 100 shapes × 5 copies, shuffled, seed 1; moisture turned on
125
+ sand = generate_scene("beach_sand", variations=Variations(moisture=True))
126
+ write_outputs(sand, "output") # -> output/beach_sand/
127
+
128
+ # 20 shapes × 10 copies, copies side by side, another random draw
129
+ gravel = generate_scene("crushed_gravel", shapes=20, copies=10, shuffle=False, seed=3)
130
+ write_outputs(gravel, "output") # -> output/crushed_gravel/
131
+ ```
132
+
133
+ ### Scene size and mixing
134
+
135
+ | Option (CLI / Python) | Default | Effect |
136
+ |---|---|---|
137
+ | `--shapes N` / `shapes=N` | 100 | number of distinct grain shapes |
138
+ | `--copies K` / `copies=K` | 5 | how many times each shape is placed; the scene has N × K grains |
139
+ | `--no-shuffle` / `shuffle=False` | shuffled | place the copies of each shape side by side, shape after shape, instead of mixing them, e.g. to compare shapes |
140
+ | `--seed S` / `seed=S` | 1 | random seed; the same options and seed give the same scene |
141
+
142
+ `--shapes` and `--copies` must be at least 1. Turning shuffling off changes
143
+ only which shape goes in which slot: shapes, positions and orientations stay
144
+ the same.
145
+
146
+ ### Presets
147
+
148
+ | Category | Presets |
149
+ |---|---|
150
+ | Sand | `beach_sand`, `dune_sand`, `volcanic_sand` |
151
+ | Gravel | `rounded_gravel`, `crushed_gravel`, `marble_chips`, `pozzolana` |
152
+ | Pebbles | `river_pebbles` (rounded, equant, earthy mixed minerals), `beach_pebbles` (flat discs and ovals, polished basalt and marble) |
153
+ | Industrial | `railway_ballast`, `iron_ore`, `coal`, `rubber_mulch` (shredded recycled tyres: torn, grippy, bouncy nuggets) |
154
+ | Agricultural | `wheat` (kernels with a ventral crease), `maize` (wedge-shaped dent kernels), `rice` (long grain, milled), `beans` (kidney-shaped, mixed varieties), `soybean`, `cherry_tomatoes` (glossy skin, stem scar): **deformable, NVIDIA OmniPhysics only** (rigid elsewhere with `--no-deformable`), **no moisture** (waxy skin) |
155
+ | Pellets | `plastic_pellets` (PE/PP, ~3.4 mm), `wood_pellets` (6 mm diameter, 10–30 mm long, broken ends) |
156
+ | Packaging | `packing_peanuts` (expanded polystyrene, S or C shaped): **deformable, NVIDIA OmniPhysics only** (rigid elsewhere with `--no-deformable`), and **no moisture**: EPS never gets wet, water runs off |
157
+ | Space | `lunar_regolith`, `martian_regolith` (no moisture) |
158
+ | Reference | `glass_beads`: exact spheres, the baseline to compare real grains against |
159
+
160
+ Values are realistic orders of magnitude, not measurements of a specific deposit.
161
+
162
+ ### Variations (checkboxes)
163
+
164
+ | Option | On | Off |
165
+ |---|---|---|
166
+ | `--size` | diameter drawn from the grading curve | all at d50 |
167
+ | `--shape` | elongation, flatness and irregularity drawn | mean values |
168
+ | `--roundness` | facets and edge rounding drawn | median values |
169
+ | `--mineralogy` | mineral and tint drawn | dominant mineral |
170
+ | `--orientation` | random orientation | all aligned |
171
+ | `--moisture` (off by default) | variant set + state drawn per grain | no moisture |
172
+
173
+ Turning one variation off does not change the other random draws: with the
174
+ same seed you compare the same scene with a single difference.
175
+
176
+ ### Moisture (OpenUSD variant set)
177
+
178
+ Every prototype carries a `moisture` variant set (`dry`, `damp`, `wet`,
179
+ `saturated`) and every grain selects its own. A state changes:
180
+
181
+ - **mass**: a wetter grain is **heavier**. Each state stores its own mass
182
+ (dry mass + water) in `physics:mass`, plus the water alone in
183
+ `granulaire:waterMass`; switching the variant switches the mass;
184
+ - **appearance**: darker colour, lower roughness, clearcoat (UsdPreviewSurface);
185
+ - **contact**: friction and restitution (one UsdPhysics material per state),
186
+ plus capillary cohesion in the custom attribute `granulaire:capillaryCohesion`,
187
+ since UsdPhysics has no standard schema for it.
188
+
189
+ The water comes from two sources: a **surface film**, which weighs a lot on
190
+ small grains (much surface for little mass) and little on pebbles, capped by
191
+ the water the pores of a heap can hold; and **absorbed water**, which
192
+ dominates for porous stone (pozzolana) and for anything that soaks
193
+ (seeds, beans, wood). Median weight gain of one grain, % of its dry mass:
194
+
195
+ | Material | Damp | Wet | Saturated |
196
+ |---|---|---|---|
197
+ | Beach sand | +3.6 % | +14 % | +24 % |
198
+ | Pozzolana (porous) | +6.2 % | +17 % | +22 % |
199
+ | Crushed gravel | +0.5 % | +1.5 % | +2.2 % |
200
+ | River pebbles | +0.2 % | +0.6 % | +0.8 % |
201
+ | Beach pebbles (dense basalt, marble) | +0.1 % | +0.3 % | +0.5 % |
202
+ | Plastic pellets | +1.0 % | +4.0 % | +7.0 % |
203
+ | Wheat | +9.6 % | +26 % | +34 % |
204
+ | Soybeans | +12 % | +34 % | +43 % |
205
+ | Wood pellets | +15 % | +41 % | +52 % |
206
+ | Common beans | +27 % | +73 % | +92 % |
207
+
208
+ Every preset's values are in `summary.json` (`median_water_content_pct`) and
209
+ per grain in `prototype_metrics.csv` (`mass_<state>_kg`).
210
+
211
+ - **No swelling.** A wet grain gets heavier but keeps its shape and size. In
212
+ reality beans, seeds and wood pellets swell when soaked, so their wet
213
+ density is overstated.
214
+ - **Indicative values.** Water contents, contact and cohesion coefficients
215
+ are model orders of magnitude, not measurements, and still need
216
+ calibrating.
217
+
218
+ In usdview or Omniverse, change a grain's state by changing its variant
219
+ selection. Blender's USD importer takes the state selected at import time.
220
+
221
+ Presets with no moisture states, even with `--moisture`: the regoliths (no
222
+ liquid water), the polystyrene packing peanuts (EPS never gets wet: water
223
+ runs off) and the cherry tomatoes (water beads on the waxy skin).
224
+
225
+ ### Deformable grains (packing peanuts, cherry tomatoes)
226
+
227
+ > **Works only with NVIDIA OmniPhysics, and not tested yet.** Deformable
228
+ > simulation of `packing_peanuts` and `cherry_tomatoes` requires an engine that
229
+ > reads NVIDIA's OmniPhysics deformable schema: Omniverse Kit and Isaac Sim 5 or
230
+ > later, where PhysX runs it on the GPU. Other tools (Blender, usdview, MuJoCo,
231
+ > Bullet, engines reading only standard UsdPhysics) open the file but see these
232
+ > grains as **static meshes with no physics**. For those, export with
233
+ > `--no-deformable` to get ordinary rigid bodies.
234
+ >
235
+ > Granulaire is developed without an NVIDIA GPU: the files are checked to be
236
+ > valid OpenUSD with the documented schemas and attributes, but they have
237
+ > **never been simulated in Isaac Sim**. In some releases the schema is a beta
238
+ > feature that must be enabled. Feedback from anyone who runs them is very
239
+ > welcome.
240
+
241
+ Why: core OpenUSD has no deformable-body schema yet. UsdPhysics covers rigid
242
+ bodies only ([UsdPhysics overview](https://openusd.org/dev/api/usd_physics_page_front.html));
243
+ the AOUSD Physics Working Group lists deformable and soft bodies only as a
244
+ possible future extension
245
+ ([charter](https://project.linuxfoundation.org/hubfs/AOUSD/Charters/24.12.16%20AOUSD%20Physics%20Working%20Group%20Charter%20-%20Approved.pdf)).
246
+ Deformable presets therefore follow NVIDIA's **OmniPhysics deformable
247
+ schema** ([reference](https://docs.omniverse.nvidia.com/kit/docs/omni_physics/latest/dev_guide/deformables/deformable_bodies.html)),
248
+ built on the core `UsdGeom.TetMesh`:
249
+
250
+ ```
251
+ /World/Prototypes/Grain_000 Xform + OmniPhysicsDeformableBodyAPI (omniphysics:mass)
252
+ geom Mesh + OmniPhysicsDeformablePoseAPI:custom (bind pose)
253
+ sim TetMesh + OmniPhysicsVolumeDeformableSimAPI
254
+ + OmniPhysicsDeformablePoseAPI:custom
255
+ + PhysicsCollisionAPI
256
+ /World/Looks/Phys Material + OmniPhysicsBaseMaterialAPI (density, friction)
257
+ + OmniPhysicsDeformableMaterialAPI
258
+ (Young's modulus, Poisson's ratio)
259
+ + PhysxDeformableMaterialAPI
260
+ (elasticity damping)
261
+ ```
262
+
263
+ - The simulation mesh fills the grain's volume with tetrahedra (voxels split
264
+ into 6 tetrahedra each, ~1,300–1,400 per grain); the render mesh follows it
265
+ through the bind pose. The tetrahedral volume matches the surface within a
266
+ few percent.
267
+ - The schemas are applied by name, so the file stays valid OpenUSD in any
268
+ tool, but only OmniPhysics acts on them. `usd-core` does not list
269
+ unregistered schemas in `GetAppliedSchemas()`, but they are in the file
270
+ (`apiSchemas` metadata).
271
+ - NVIDIA's schema is still evolving (it replaced an earlier PhysX deformable
272
+ API); if a newer Omniverse release renames attributes, re-export with an
273
+ updated Granulaire. If OpenUSD standardises deformables, the export will move
274
+ to that schema.
275
+ - `--no-deformable` exports the same presets as rigid bodies.
276
+ - **Bounce.** Deformable materials have no restitution: a grain bounces
277
+ through its own elasticity, and `physxDeformableMaterial:elasticityDamping`
278
+ (1/s) sets how much energy each deformation loses. The values below are
279
+ uncalibrated starting points, to tune in Isaac Sim.
280
+ - Material values are indicative:
281
+
282
+ | Preset | Young's modulus E | Poisson's ratio ν | Elasticity damping |
283
+ |---|---|---|---|
284
+ | `packing_peanuts` (EPS foam) | ≈ 1 MPa | ≈ 0.1 | 0.02 /s |
285
+ | `cherry_tomatoes` (soft fruit, keeps its volume when squeezed) | ≈ 0.5 MPa | ≈ 0.45 | 0.01 /s |
286
+
287
+ ## Outputs (per preset, under `output/<preset>/`)
288
+
289
+ | File | Content |
290
+ |---|---|
291
+ | `granulaire_<preset>.usdc` | OpenUSD scene (Blender: File > Import > USD) |
292
+ | `prototype_metrics.csv` | measured descriptors, mass (per moisture state), parameters |
293
+ | `instances.csv` | grain → shape, position, moisture state |
294
+ | `summary.json` | global statistics (d50, mean sphericity, masses, water) |
295
+ | `prototypes.npz` | meshes, used by the renderer |
296
+ | `shape_sheet.png`, `mix_preview.png` | previews (`granulaire render`) |
297
+
298
+ ### In Blender
299
+
300
+ The `.usdc` scene opens directly in Blender (File > Import > USD).
301
+
302
+ <table>
303
+ <tr>
304
+ <td width="50%"><img src="https://raw.githubusercontent.com/drscaon/granulaire/main/docs/images/blender_gravel.jpg" alt="Crushed gravel grains imported into Blender, falling onto the floor under rigid-body physics"></td>
305
+ <td width="50%"><img src="https://raw.githubusercontent.com/drscaon/granulaire/main/docs/images/blender_sand.jpg" alt="Beach sand grains imported into Blender, small rounded grains in varied mineral colours"></td>
306
+ </tr>
307
+ <tr>
308
+ <td align="center"><sub>Crushed gravel falling onto the floor (Blender rigid-body physics)</sub></td>
309
+ <td align="center"><sub>Beach sand, rounded grains in mineral colours</sub></td>
310
+ </tr>
311
+ </table>
312
+
313
+ <sub>Screenshots from the first version (0.1.0); grain shapes have become more
314
+ varied since then.</sub>
315
+
316
+ ## Method in brief
317
+
318
+ Each grain is an implicit volume, meshed with marching cubes:
319
+
320
+ 1. **Base body.** Rocks: a random convex polyhedron (the hull of points drawn
321
+ on an ellipsoid with Zingg proportions; few facets give an angular grain),
322
+ with edges rounded by replacing the *max* in its gauge with a p-norm (low p
323
+ gives a rounded grain, high p sharp edges). Kernels and seeds: a
324
+ superellipsoid. Pellets: a cylinder with rounded end edges.
325
+ 2. **Fracture chips.** Random planes cut the body, leaving flat broken faces
326
+ with sharp edges (crushed stone, broken pebbles, broken pellet ends).
327
+ 3. **Taper and irregularity.** An egg or wedge taper along the long axis, then
328
+ low-frequency lumps, pits (scoria vesicles, flint dents), a crease (wheat),
329
+ a crown dent (maize), a kidney bend with its hilum (beans) and light
330
+ roughness.
331
+ 4. **Mesh and scale.** Marching cubes on the zero level set, recentring on the
332
+ centre of mass, scaling to the target nominal diameter.
333
+
334
+ ## Friction and restitution
335
+
336
+ Every grain gets a standard OpenUSD physics material (`UsdPhysicsMaterialAPI`:
337
+ static friction, dynamic friction, restitution), bound to its collision mesh
338
+ with the `physics` purpose. With moisture enabled there is one material per
339
+ state, and each grain's `moisture` variant selects it. The ground has its own
340
+ material (static friction 0.60, restitution 0.10).
341
+
342
+ ### Values
343
+
344
+ Dynamic friction is 90 % of static friction. In wet states, friction is
345
+ multiplied by 1.10 (damp: a little water makes grains grip), 1.00 (wet) and
346
+ 0.80 (saturated: grains slide on the water); restitution by 0.60, 0.40
347
+ and 0.30. The values are realistic orders of magnitude, ranked sensibly
348
+ (angular crushed rock grips more than round, polished or waxy grains), not
349
+ measurements of a specific deposit.
350
+
351
+ | Preset | Static μ (dry) | Dynamic μ (dry) | Restitution (dry) | Static μ damp | Static μ wet | Static μ saturated |
352
+ |---|---|---|---|---|---|---|
353
+ | `beach_sand` | 0.55 | 0.50 | 0.10 | 0.61 | 0.55 | 0.44 |
354
+ | `dune_sand` | 0.50 | 0.45 | 0.10 | 0.55 | 0.50 | 0.40 |
355
+ | `volcanic_sand` | 0.60 | 0.54 | 0.10 | 0.66 | 0.60 | 0.48 |
356
+ | `rounded_gravel` | 0.55 | 0.50 | 0.15 | 0.61 | 0.55 | 0.44 |
357
+ | `crushed_gravel` | 0.65 | 0.59 | 0.15 | 0.72 | 0.65 | 0.52 |
358
+ | `marble_chips` | 0.60 | 0.54 | 0.15 | 0.66 | 0.60 | 0.48 |
359
+ | `pozzolana` | 0.75 | 0.68 | 0.10 | 0.83 | 0.75 | 0.60 |
360
+ | `river_pebbles` | 0.50 | 0.45 | 0.20 | 0.55 | 0.50 | 0.40 |
361
+ | `beach_pebbles` | 0.40 | 0.36 | 0.25 | 0.44 | 0.40 | 0.32 |
362
+ | `railway_ballast` | 0.70 | 0.63 | 0.15 | 0.77 | 0.70 | 0.56 |
363
+ | `iron_ore` | 0.70 | 0.63 | 0.12 | 0.77 | 0.70 | 0.56 |
364
+ | `coal` | 0.55 | 0.50 | 0.10 | 0.61 | 0.55 | 0.44 |
365
+ | `rubber_mulch` | 0.85 | 0.77 | 0.55 | 0.94 | 0.85 | 0.68 |
366
+ | `wheat` | 0.40 | 0.36 | 0.40 | 0.44 | 0.40 | 0.32 |
367
+ | `maize` | 0.45 | 0.41 | 0.35 | 0.50 | 0.45 | 0.36 |
368
+ | `rice` | 0.40 | 0.36 | 0.30 | 0.44 | 0.40 | 0.32 |
369
+ | `beans` | 0.35 | 0.32 | 0.40 | 0.39 | 0.35 | 0.28 |
370
+ | `soybean` | 0.35 | 0.32 | 0.45 | 0.39 | 0.35 | 0.28 |
371
+ | `cherry_tomatoes` | 0.45 | 0.41 | 0.20 | — | — | — |
372
+ | `plastic_pellets` | 0.30 | 0.27 | 0.50 | 0.33 | 0.30 | 0.24 |
373
+ | `wood_pellets` | 0.50 | 0.45 | 0.20 | 0.55 | 0.50 | 0.40 |
374
+ | `packing_peanuts` | 0.50 | 0.45 | 0.30 | — | — | — |
375
+ | `lunar_regolith` | 0.75 | 0.68 | 0.05 | — | — | — |
376
+ | `martian_regolith` | 0.70 | 0.63 | 0.05 | — | — | — |
377
+ | `glass_beads` | 0.35 | 0.32 | 0.85 | 0.39 | 0.35 | 0.28 |
378
+
379
+ — : no moisture states for this preset. Packing peanuts and cherry tomatoes
380
+ are deformable: NVIDIA OmniPhysics ignores static friction for deformables, so
381
+ their single value is written as both static and dynamic friction, and
382
+ restitution does not apply (their bounce comes from elasticity and damping, see
383
+ [Deformable grains](#deformable-grains-packing-peanuts-cherry-tomatoes)); the
384
+ restitution shown is used when they are exported as rigid bodies
385
+ (`--no-deformable`).
386
+
387
+ ### Which engines use it
388
+
389
+ | Tool | Friction and restitution from the USD file |
390
+ |---|---|
391
+ | NVIDIA Isaac Sim / Omniverse (PhysX) | **Used**, including the material selected by each grain's moisture variant. PhysX averages the two materials in contact by default (`physxMaterial:frictionCombineMode` to change it). |
392
+ | Other engines importing `UsdPhysics` (e.g. Newton) | Should be read, as it is the standard schema; not tested here. |
393
+ | Blender | **Not used**: Blender's USD importer does not import physics data. Set friction in the rigid-body panel or with an import script. |
394
+ | MuJoCo, Gazebo, PyBullet | Not native; depends on the converter. MuJoCo combines friction differently (it takes the larger of the two). |
395
+
396
+ ### Limits
397
+
398
+ - **Friction is per pair of surfaces.** What a robot foot feels also depends on
399
+ the foot's own material and the engine's combine mode.
400
+ - **No capillary cohesion in simulation.** Wet-grain cohesion is stored in
401
+ `granulaire:capillaryCohesion`, but OpenUSD has no standard schema for it and
402
+ no rigid-body engine reads it: in simulation, wet grains differ only by
403
+ friction, restitution and mass.
404
+ - **No rolling resistance.** Neither `UsdPhysics` nor PhysX rigid bodies model
405
+ it, which is why piles of spheres flow far too easily. Here the irregular
406
+ shapes play that role: an angular grain cannot roll.
407
+ - **Calibrate for quantitative work.** For training with domain
408
+ randomisation, these values are a sensible centre to randomise around; for a
409
+ quantitative study, calibrate them against the real material, for example
410
+ with its angle of repose.
411
+
412
+ ## Physics limits by grain size
413
+
414
+ Grains are generated at their **real size**. That is right for rendering and
415
+ perception, but many of them are too small or too light for today's
416
+ rigid-body engines with default settings. Rules of thumb behind the table:
417
+
418
+ - **Bullet** (PyBullet, Blender) works best with objects larger than ~0.1 m,
419
+ and Blender's default collision margin is 0.04 m.
420
+ - **PhysX** (Isaac Sim, Omniverse) uses a default `contactOffset` of 0.02 m
421
+ per shape at metric scale, so two shapes are treated as touching from ~4 cm.
422
+ - **Tunnelling**: at 60 Hz, a grain falling at 1–2 m/s travels 1.7–3.3 cm per
423
+ step and can pass through surfaces smaller than that without CCD or substeps.
424
+ - **Mass ratio**: iterative solvers become unstable when very light grains
425
+ touch a much heavier body, such as a robot foot weighing a few kilograms.
426
+
427
+ | Physics risk | Presets (median nominal size · mean grain mass) | What to do |
428
+ |---|---|---|
429
+ | **Not usable as individual rigid bodies** | `lunar_regolith` (0.07 mm · ~13 µg), `martian_regolith` (0.12 mm · ~0.1 mg), `dune_sand` (0.22 mm · ~18 µg), `beach_sand` (0.36 mm · ~0.1 mg), `volcanic_sand` (0.49 mm · ~0.4 mg), `glass_beads` (1 mm · ~1.4 mg) | Use for rendering and perception; for physics, use a continuum model (MPM), a deformable heightfield, or scaled-up grains |
430
+ | **Unstable without tuning** | `rice` (2.8 mm, 1.6 mm thick · ~18 mg), `plastic_pellets` (3.4 mm · ~20 mg), `wheat` (4.1 mm · ~53 mg), `rounded_gravel` (5.8 mm · ~0.4 g), `soybean` (6.6 mm · ~0.19 g), `beans` (9.1 mm, 5.8 mm thick · ~0.53 g), `maize` (7.4 mm, 4.7 mm thick · ~0.29 g), `crushed_gravel` (8.2 mm · ~1 g) | Reduce contact offsets / margins to ~1 mm, enable CCD, use substeps or a smaller time step |
431
+ | **Workable with tuning** | `packing_peanuts` (20 mm, ~9 mm thick · ~50 mg, extremely light), `wood_pellets` (9.8 mm, 6 mm thick · ~0.6 g), `pozzolana` (11 mm · ~1.5 g, very light), `marble_chips` (11.5 mm · ~2.5 g), `iron_ore` (16 mm · ~17 g), `rubber_mulch` (20 mm, 12 mm thick · ~6 g), `cherry_tomatoes` (25 mm · ~8 g, deformable) | Same settings as above; check stability against the heaviest body in contact |
432
+ | **Generally fine** | `coal` (23 mm · ~15 g), `beach_pebbles` (38 mm, 18 mm thick · ~140 g), `railway_ballast` (40 mm · ~100 g), `river_pebbles` (46 mm · ~230 g) | Keep CCD on for impacts; in Blender, lower the default 0.04 m margin |
433
+
434
+ For elongated or flat grains (rice, maize, beans, pellets), the **thickness** is what
435
+ matters for tunnelling, not the nominal size. Packing peanuts are large but
436
+ weigh almost nothing: the mass ratio with anything they touch is the main risk,
437
+ and as deformable bodies they are simulated only by NVIDIA OmniPhysics (see
438
+ above).
439
+
440
+ These are orders of magnitude, not hard limits: the actual threshold depends
441
+ on the engine, time step, solver iterations, contact settings and the bodies
442
+ in contact. Scaling grains up changes the physics (mass, inertia, contact
443
+ forces scale differently), so treat it as an approximation of the material.
444
+
445
+ **Perception is not affected.** Cameras and renderers have no minimum size:
446
+ every preset, including sand and regoliths, can be used at real scale for
447
+ synthetic images, ground-truth masks, grading-from-image benchmarks or
448
+ appearance studies (moisture darkening and gloss). Only dynamics need the
449
+ precautions above. A thin layer of a few hundred grains on a hard surface
450
+ (the "sand on the ramp" case) is also within reach of rigid-body engines for
451
+ the gravel and pebble presets.
452
+
453
+ ## Known limits
454
+
455
+ - **Real units.** See *Physics limits by grain size* above.
456
+ - **No grain-by-grain physics at large scale.** CPU physics targets a few
457
+ hundred grains; instancing is for rendering surfaces.
458
+ - **Angularity is a proxy**, not Wadell roundness, and is only comparable
459
+ between grains of the same mesh resolution.
460
+ - **Collision uses the convex hull** for rigid grains. Pits, creases and dents
461
+ are visible but not part of the collision shape; hollow shells (whole
462
+ seashells) are not generated. Deformable grains collide with their
463
+ tetrahedral mesh.
464
+ - **Deformables work only with NVIDIA OmniPhysics, untested.** Until OpenUSD
465
+ standardises deformable bodies, packing peanuts and cherry tomatoes are
466
+ simulated as deformables only in Omniverse / Isaac Sim (GPU); elsewhere they
467
+ are static meshes unless exported with `--no-deformable`. They have not been
468
+ run in Isaac Sim yet.
469
+ - **Mesh resolution.** Marching cubes runs on a 48-cell grid along the longest
470
+ axis; the sharpest edges can show slight stair-stepping.
471
+
472
+ ## Development
473
+
474
+ ```bash
475
+ pip install -e ".[dev]"
476
+ pytest # test suite
477
+ ruff check . # lint
478
+ ruff format . # format
479
+ ```
480
+
481
+ ### Commits and versions
482
+
483
+ - Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/)
484
+ (`feat:`, `fix:`, `docs:`, `test:`, `refactor:`, `build:`, `chore:`).
485
+ - Versions follow [Semantic Versioning](https://semver.org/). The version
486
+ lives in one place, `pyproject.toml`; `granulaire.__version__` reads it from
487
+ the installed package metadata.
488
+ - To release: update `version` in `pyproject.toml`, move the `Unreleased`
489
+ entries of `CHANGELOG.md` under the new version with its date, commit as
490
+ `chore(release): X.Y.Z`, then tag `vX.Y.Z` (annotated). Pre-releases use
491
+ PEP 440 suffixes (`0.2.0b1` for a beta, tag `v0.2.0b1`).
492
+
493
+ ### Branches (GitHub Flow)
494
+
495
+ The repository follows [GitHub Flow](https://docs.github.com/en/get-started/using-github/github-flow):
496
+
497
+ 1. `main` is the only long-lived branch and is always releasable. Nobody
498
+ pushes to it directly.
499
+ 2. Every change starts on a short-lived branch from `main`, named after its
500
+ type: `feat/<name>`, `fix/<name>`, `docs/<name>`, `ci/<name>`, …
501
+ 3. Push the branch and open a pull request into `main`; CI runs on it.
502
+ 4. Merge when CI is green, then delete the branch.
503
+
504
+ Releasing is a change like any other: on a `chore/release-X.Y.Z` branch, bump
505
+ `version` in `pyproject.toml` and move the `Unreleased` entries of
506
+ `CHANGELOG.md` under the new version; merge the pull request, then tag
507
+ `vX.Y.Z` on `main` (or create the release on the GitHub web page with that new
508
+ tag). The release workflow builds the packages and attaches them.
509
+
510
+ A versioned `pre-push` hook refuses direct pushes to `main`. Enable it once per
511
+ clone with `git config core.hooksPath .githooks` (emergency override:
512
+ `ALLOW_MAIN_PUSH=1 git push`).
513
+
514
+ On a private repository, GitHub enforces branch protection rules only with
515
+ GitHub Pro (or on a public repository). With either, add a rule on `main`:
516
+ require a pull request and the CI checks to pass, block force pushes and
517
+ deletion.
518
+
519
+ ### Continuous integration
520
+
521
+ GitHub Actions runs these workflows (`.github/workflows/`):
522
+
523
+ | Workflow | When | What |
524
+ |---|---|---|
525
+ | `ci.yml` | every push to `main`, every pull request | `ruff check` and `ruff format --check`; `pytest` on Python 3.10–3.13 (Linux) and 3.12 (macOS, Windows); a command-line smoke test; build the wheel and sdist, `twine check`, install the wheel in a clean environment. The packages are kept as a run artifact for 14 days. |
526
+ | `release.yml` | a pushed tag `v*` | checks the tag matches the version in `pyproject.toml`, tests, builds, then creates the GitHub release with the packages attached and the notes taken from `CHANGELOG.md`. Versions with `a`, `b` or `rc` are marked as pre-releases. If the release already exists (created on the web page), the packages are added to it. |
527
+ | Dependabot | monthly | pull requests to update the GitHub Actions and the Python dependencies. |
528
+
529
+ Ruff is pinned in CI so that a new ruff release cannot fail the build on
530
+ unchanged code; update the pin and run `ruff format .` together.
531
+
532
+ ## License
533
+
534
+ [MIT](https://github.com/drscaon/granulaire/blob/main/LICENSE) © 2026 Daniel Régis Sarmento Caon
535
+
536
+ The logo is set in IBM Plex Sans (SIL Open Font License 1.1), outlined into
537
+ paths, so it displays without the font installed.
538
+
539
+ ---
540
+
541
+ Granulaire is part of [**Primvaria**](https://github.com/drscaon/primvaria), physics-ready variability for Physical AI, in OpenUSD: `pip install primvaria` installs the whole suite.