What you can do
You can run the press itself, then measure what it cost you. One call compacts a generated packing to a target strain and hands back the compacted structure. Everything you would normally measure works on that structure.
- POST /api/v1/pbd-compaction takes a sphere packing recipe, a target strain and a friction coefficient.
- It returns per-step metadata and the final voxels, so you can see where the plate ended up.
- POST /api/v1/metrics gives solid fraction and tortuosity along each axis.
- POST /api/v1/permeability and POST /api/v1/conductivity give the flow and conduction numbers for the same structure.
- A compaction call at 48³ came back in 623 to 1,058 ms across the six runs.
That makes the press a parameter you can sweep instead of a process you inherit. The useful output is not one number but the rate at which each number moves.
Why it matters
Pressing a powder is one knob with one setting on it. Calendering an electrode is the usual example, and density is the thing that gets checked on the line. What the same press does to transport is much harder to see.
Modelling the compaction step directly is an active topic, and porosity and tortuosity are increasingly read as a pair. The question that stays open is where to stop. Each point of porosity you press out costs something, and the costs do not arrive at the same rate.
One packing, six press settings
Everything is held fixed except the strain. The recipe packs 320 spheres of 4 µm radius into a 48³ box at 1 µm per voxel, targeting 35 % loading with 10 % overlap, on one seed.
- Target strains 0, 0.05, 0.10, 0.20, 0.30 and 0.40, with the friction coefficient held at 0.3.
- The strain 0 point runs through the same compaction path as the others, so it is the baseline everything is compared against.
- Every run reported ten steps of thirty solver iterations, carrying 320 particles.

The grid does not shrink when the plate comes down. At strain 0.40 the plate stopped at z = 28.8 but the returned structure is still 48 voxels tall. The response tells you where the plate is, in the topPlateZ field of the last snapshot.
Two crops before any number is worth reading
Measure the returned structure as it arrives and every number is wrong. Two cuts fix it, and the second one is the surprise.
- Cut the empty grid above the plate. At strain 0.40 that is 20 of the 48 layers, and leaving it in reports air as pore space.
- Cut two voxels off each side wall. The packing never touches the walls, so a gap runs the full height of the box around the perimeter.
The wall gap does real damage to one number. With the walls in, tortuosity along z read exactly 1.000 at all six strains. The shortest path and the straight-line distance were identical every time, which means something was open from bottom to top with no sideways step at all.

Of the 97 open columns at strain 0.40, 96 lie on the outermost ring of voxels. Trim two voxels from each lateral side and no straight column survives at any strain. Tortuosity then reads 1.17 at strain 0 and climbs from there.
Permeability pays first, and pays most
Permeability is the number that pays for the density. Over the six structures porosity fell by 1.6 times. Permeability fell by 5.9 times.
| Strain | Slab height (voxels) | Porosity | Tortuosity, z | Permeability, z (µm²) | Conductivity, z (W/m·K) |
|---|---|---|---|---|---|
| 0 | 48 | 25.9 % | 1.170 | 0.132 | 4.69 |
| 0.05 | 45 | 24.0 % | 1.159 | 0.074 | 4.65 |
| 0.10 | 43 | 23.6 % | 1.167 | 0.099 | 4.64 |
| 0.20 | 38 | 20.1 % | 1.216 | 0.055 | 4.50 |
| 0.30 | 33 | 18.3 % | 1.313 | 0.037 | 4.26 |
| 0.40 | 28 | 16.3 % | 1.296 | 0.022 | 3.82 |
Tortuosity is flat until strain 0.10, then climbs. It is the only one of the four that goes up. The path through the pore space really does get longer, but slowly.
Effective conductivity is the odd one out. It fell while solid fraction rose from 74.1 % to 83.8 %, which is the opposite of what more solid should do. The reduced graph the solver builds also shrank from 510 nodes to 184 over the sweep, so confirm this curve against a full-grid solve before you lean on it.
The practical reading is about where the press stops being worth it. If flow through the pore space is what you are protecting, the first 0.10 of strain already costs you a quarter of it. If conduction through the solid is what you are buying, this packing did not sell you any.
Friction moved porosity more than another 0.10 of strain
Holding the strain at 0.20 and sweeping friction changed porosity by 3.3 points. Going from strain 0.20 to 0.30 at fixed friction changed it by 1.9 points. The friction coefficient is not a second-order setting here.
| Friction coefficient | Porosity | Max particle overlap (µm) | Tortuosity, z |
|---|---|---|---|
| 0 | 21.7 % | 8.00 | 1.243 |
| 0.3 | 20.1 % | 8.00 | 1.216 |
| 0.6 | 19.2 % | 8.00 | 1.216 |
| 1.0 | 18.4 % | 7.65 | 1.270 |
The plate stopped at the same height in all four runs, so the box volume was identical. What changed is how far the particles pushed into each other. The friction 1.0 run is the only one whose maximum pairwise overlap stayed under 8 µm, and less interpenetration leaves more of the box counted as solid.
Running it on your own recipe
Send a sphere packing recipe and a strain, read topPlateZ off the last snapshot, crop, then measure. The recipe here started from the particle-packing entry in GET /api/v1/examples, with the grid, seed, loading and overlap overwritten.
POST /api/v1/pbd-compaction
{
"recipe": {
"generatorType": "particlePacking",
"grid": { "nx": 48, "ny": 48, "nz": 48, "voxelSizeUm": 1 },
"geometryParams": {
"fillers": [ { "type": "sphere", "radius": 4, "volumeFraction": 35, "materialId": 1 } ],
"particles": [ { "type": "sphere", "radius": 4, "volumeFraction": 35, "materialId": 1 } ]
},
"volumeFraction": 35,
"overlapPercent": 10,
"seed": 20260927
},
"targetStrain": 0.40,
"frictionCoefficient": 0.3
}
-> snapshots[last].topPlateZ = 28.8
-> finalStructure.grid.nz = 48 // unchanged; crop to z < 28
POST /api/v1/structures -> structureId, for the cropped structure
POST /api/v1/metrics { "structureId": "<id>" }
-> metrics.tortuosity.z.ratio
The loading has to be written into several places at once. The generator reads geometryParams, fillerConfigs and distributionRules, so setting only the top-level volumeFraction leaves the packing at whatever the example shipped with.
Keep the grid modest. The response carries finalStructure.voxels as a plain array, so 64³ and above runs into the body size limit. At 48³ the whole first pass came to 31 requests and 13 seconds of API time.
Include strain 0 in the sweep. It goes through the same compaction and voxelisation path as every other point, which a structure from POST /api/v1/generate does not. Comparing against the wrong baseline is the easiest way to invent a trend.
What this does not settle
Each point is a shorter slab than the one before it, from 48 layers down to 28. That is what pressing does, but it means the six measurements are not a fixed window onto a changing material.
The permeability curve wobbles at strain 0.10. The top face is a cut surface, and the open pore area it exposes went 994, 613, 822, 545, 412, 284 voxels across the six strains. Permeability tracks that wobble, so where the crop plane lands is part of the answer.
This is one seed, one particle size and one recipe. The ordering of the four curves is the result worth carrying over. The individual numbers are this packing only.