Shape optimization with neural implicit geometry and adjoint CFD sensitivities. A design is a DeepSDF latent lattice (or a free-form deformation) inside a design box; every iteration the body-fitted hex mesh is generated directly from the signed distance field, OpenFOAM's continuous adjoint or DAFoam's discrete adjoint returns the wall sensitivities, and the method of moving asymptotes (MMA) takes a step under CFD and geometric constraints.
Installation and machine setup: INSTALL.md.
uv run deepshapeopt optimize --config experiments/drag_cube/config_latent_cube.jsonThe config sections mirror the building blocks of a run:
| Section | Block | Options |
|---|---|---|
run |
name, device, iterations, debug, heavy-data and scratch directories | |
geometry |
input mesh, length unit, design domain, flow type | internal / external |
parametrization |
design variables | deepsdf (latent lattice, optional PCA reduction), ffd; locked control points |
mesh |
the sdf_hex mesher: octree castellation and differentiable snap onto the SDF |
caps, patches, refinement |
solver |
forward solver and adjoint | openfoam (continuous, ESI), dafoam (discrete) |
objective |
CFD metric plus penalties | drag, uniformity, uniformity_directional, losses; proximity, lattice smoothness |
constraints |
MMA rows with budgets | CFD metric, volume, centroid, FFD fold-over guard, undercut, minimum steg length, no thinning in a painted region |
optimizer |
MMA settings | move limit, bounds, GCMMA inner loop, feasibility restoration, step control, convergence stop |
diagnostics |
run mode and exports | optimize, noise_probe, jacobian_probe; VTK / STL series |
The no_thinning constraint protects a region in which the start design may only gain solid.
The region is a 2-D mask extruded along one axis, painted on a projection of the start design:
uv run deepshapeopt paint-mask --config <config>.json --out data/masks/<name>.jsonA new --out starts from an empty mask and an existing one is loaded for editing; the start
design comes from the run's rec_parameters.pt (or --params-file), and --axis, --device and
--port set the extrusion axis, the device and the server port. The painter serves on
http://127.0.0.1:8001. Under Mirror, the two in-plane axes can be mirrored at planes through the
lattice-box centre (the planes of parametrization.deepsdf.symmetry; the axes listed there are
checked at start). The brush and eraser then act on every mirror image, the active planes are drawn
as dashed lines with a ghost cursor on each image, Symmetrize mask adds the mirror image of the
current mask, and the panel counts the pixels without a mirror partner. The symmetry is stored in
the painted pixels, so the mask file and the constraint are unchanged. Paint on solid only (key S,
on by default) lets the brush set only pixels that are solid in the start design. That means
solid in at least one section along the axis, or at least the min. solid share of the
sections; within the extrusion range once both ends are set. Clip mask to solid removes painted
pixels that fail that test, and the panel counts pixels on fluid (drawn hatched); for
no_thinning such pixels do nothing.
Older configs (two blocks reconstruction / optimization) convert with
uv run deepshapeopt migrate-config --write <config>.
The result of a run exports as one watertight STL of the full geometry: the input geometry
(geometry.mesh_path, or mesh.sdf_hex.geometry_stl) with the optimized design in the design
domain. A half model (a domain face with patch_types symmetry / symmetryPlane) is mirrored,
so the STL is always the full body:
uv run deepshapeopt export-stl --config <config>.json [--params <file>.pt] [--part full|design] \
[--out <file>.stl] [--resolution 256] [--blend <width>] [--device cpu]| option | default | meaning |
|---|---|---|
--params |
<run.name>/optimization/parameters.pt |
parameter file; any saved design works, e.g. reconstruction/rec_parameters.pt for the reconstructed start design |
--part |
full |
full: the closed full geometry; design: only the surface inside the design domain (the half the mesh keeps, not mirrored), cut exactly at the design-domain faces: the counterpart of the run's current_shape.stl, meshed like the full geometry |
--out |
optimized_<part>.stl next to the parameter file |
STL to write, in the geometry unit (no unit_to_metre scaling) |
--resolution |
256 | marching-cubes cells along the longest design-domain side (DeepSDF, spline SDF) |
--blend |
4 % of the longest design-domain side | width of the transition into the input geometry at the design-domain faces it passes through; 0 switches it off |
--device |
run.device |
device for the design SDF evaluation |
parameters.pt is written after the last optimizer step, i.e. one step past the last design the
solver evaluated. Results in the old v1 layout (<run.name>/*/optimization/updated_parameters.pt)
are found as well.
How the geometry is assembled:
- DeepSDF and spline SDF. The design SDF is sampled on a uniform grid two cells beyond the design domain and
meshed with marching cubes; an exact boolean (manifold3d) cuts the piece at the design domain and
joins it with the input STL outside, which stays untouched. Towards every design-domain face that
the input passes through, the design SDF is blended into the exact signed distance of the input
(smoothstep over
--blend), so the piece meets the input without a step: the reconstruction error at the locked faces is absorbed within the band instead of leaving a seam. The log reports that step per face and warns when the design departs from the input by more than a quarter of the band there (a face that is not locked); the blend then reshapes the design near that face. The mesh inside the design domain is finer and smoother than the triangulation of the input STL, so the change of tessellation at the faces stays visible in shading. - FFD. The input STL is deformed directly (the vertices inside the design domain and their mirror images), so the result keeps its triangulation and needs no remeshing or blending.
- Symmetry. If the input extends across the symmetry plane, only the design is mirrored and replaces the input on the other side as well; if the input ends at the plane, the assembled body is mirrored and stitched at the plane.
The log closes with the number of triangles and bodies, the volume and the bounds, and warns if the result is not a single closed body.
deepshapeopt/
driver.py, problem.py, cli.py the optimization loop, its assembly from a config, the command line
config/ schema v2, loader, run paths, v1 migration
geometry/ domain frame, lattice reconstruction, reconstruction analysis, projected masks
parametrization/ DeepSDF lattice, PCA basis, FFD, locked control points, design space
hexmesh/ sdf_hex: octree, snap, polyMesh writer, caps and outlet sub-patches
mesher.py MeshResult and the mesher wrapper the loop uses
solvers/ metric registry; openfoam/ (runtime, sensitivities, export); dafoam/ (runner, in-container script)
terms/ objective, penalties and constraint rows (CFD, volume, centroid, FFD jacobian, undercut, steg length, no thinning)
optimizer/ MMA wrapper (DeepSDFStruct.optimization.MMA), step control, convergence stop
diagnostics/ history CSV, plots, exports, probes, path consistency, noise estimate, latent metric
latent_gui/ web editor for the latent codes of a saved design
mask_painter/ brush editor for the protected region of the no_thinning constraint
openfoam/ OpenFOAM user libraries (directional uniformity objective, powerLaw-Arrhenius material model)
external/dafoam_patches/ DAFoam patch (fvMatrix preconditioner) and its build notes
experiments/ drag_cube/ (external flow), channel/ (internal flow), reconstruction/ (rim, shiba, propeller, channel)
tests/ pytest (CPU only) and tests/smoke/ end-to-end configs
scripts/ optimize.py, reconstruct.py, latent_gui.py (thin wrappers around the CLI), publish_dafoam_build.sh
slurm/ generic SLURM template, config as argument
experiments/drag_cube/: drag of a cube-like body (DeepSDF lattice with and without PCA, FFD, OpenFOAM and DAFoam), the case templatefoam_case/anddafoam_case/.experiments/channel/: internal flow through a branching channel, outlet velocity uniformity under a pressure-loss constraint (DeepSDF lattice and FFD).experiments/reconstruction/: DeepSDF reconstructions of public geometries (rim, shiba, ship propeller from Thingiverse, and the channel).
Each run writes <experiment>/<run.name>/{reconstruction,optimization}/ with current_shape.stl,
parameters.pt, optimization_history.csv, plots and run.log; with run.debug the VTK / STL
series go to run.heavy_data_dir. Iteration numbering starts at 0.
The results of the companion paper (citation to follow) were produced with the archived snapshots DeepShapeOpt 10.5281/zenodo.20210465 and DeepSDFStruct 10.5281/zenodo.20210456 (concept DOIs 10.5281/zenodo.20210464 and 10.5281/zenodo.20205817). Those snapshots contain the snappyHexMesh path used for the paper runs, which this version replaced by the SDF hex mesher. Training data for the decoder: 10.48436/12y18-j6236 (only needed to retrain).
Cite the archived release (DeepShapeOpt v0.1.0, https://doi.org/10.5281/zenodo.20210464); a
machine-readable citation is in CITATION.cff.
Reconstruction geometries: My little shiba by layerone (thing:1308678), Parametric Ship Propeller by Fysik_klubben (thing:6906448), RC Rim by Attila_d (thing:1328760), all Thingiverse.
See LICENSE.