The engine is SatForm3D. The dataset it produces is SHAPE — Spacecraft Hierarchies, Articulation, Physics, Ensembles.
Dataset: SHAPE on Zenodo, https://doi.org/10.5281/zenodo.23892018 (CC BY 4.0). Data descriptor: Zapevalin, P. R. A synthetic dataset of articulated 3D spacecraft models with optical and mass properties (in preparation).
A data-driven generative engine for physically-consistent synthetic spacecraft geometry. Given a declarative JSON catalog, SatForm3D procedurally assembles diverse, structurally-distinct 3-D satellite models — bus + subsystem components
- articulated deployables — enforces physical consistency (collision, keep-out, swept-motion gates), and exports them with per-facet optical properties (ρ, s, ε) and mass/inertia for light-curve / solar-radiation-pressure (SRP) modelling and ML pose / inspection algorithms.
It exists to fill the gap left by classified real-spacecraft CAD: a large,
labelled, physically-consistent synthetic dataset that samples the spacecraft
design space itself. That dataset is SHAPE, and the four words behind the
acronym are the four things a record carries beyond a mesh: a retained component
hierarchy, an articulation graph certified across its motion envelope,
physics (per-facet optics plus mass, centre of mass and inertia), and an
ensemble of 34,000 designs rather than a single object. Naming policy:
docs/NAMING.md.
- Version 2.3.0 · HDF5 schema v2.2 · C++20 · Apache-2.0
- 11 bus archetypes × 8 subsystems × 42 component types × 62 grammar rules, composed by a probabilistic assembly grammar with size/parameter variation.
- 12 mission-conditioned regimes + an unconditioned exploratory regime, each gated
independently on up to 8 statistical-realism criteria. Current result: 13 of 13
regimes pass every criterion scored for them — 8 of 8 for the unconditioned regime, 7
of 7 for eleven mission regimes, 6 of 6 for
spent_stage(realism_gate, 300 models per regime, seed 12345). The size bands are derived from an external corpus of 134 cited real spacecraft, 105 of which carry a usable size, not from the generator's own output — a band read off the population it judges cannot fail by construction. Each band carries the number of spacecraft behind it and its leave-one-out coverage, and the report prints both: every regime band is built on at least 3 spacecraft, the fewest being 3 fornav_meo, 4 forcubesatandbody_mountedand 5 forgeo_comms. - 9 physical-consistency validators (no interpenetration, no swept-articulation
collision, keep-out clear, outward normals, single connected assembly, plausible
mass properties, per-facet optical consistency, functional-surface exposure, attached parts) — the
property gate runs with zero failures, and
satform generate --validateapplies the same pipeline. - Articulation graph: single-DOF revolute/prismatic joints, named deployment
states, and kinematic couplings (
q = a·q_driver + b). - Reproducible: every model is seeded, with a
/provenancerecord (seed, model index, generator version, and a SHA-256 digest of the config directory). - 9 named real-spacecraft reference models (Intelsat 29e, Iridium-NEXT, Sentinel-2, Jason-3, Envisat, GRACE-FO, 3U CubeSat, Hubble, Voyager 1).
- Interoperable I/O: HDF5 (lossless master) + glTF 2.0 / GLB / URDF / OBJ, plus a bidirectional converter to STL, PLY, and VTK/VTP.
- Embedded WebGL viewer and a Streamlit GUI (
SatForm3D Studio).
| Path | Contents |
|---|---|
src/ |
C++20 engine (core scene graph, assembly, physics, IO, CLI) |
config/ |
Data-driven catalog: archetypes, components, grammar, missions, realism |
config_named/ |
config/ plus five part types for hand-written craft (docs/NAMED_SATELLITES.md) |
tests/ |
Unit, golden and property tests run by ctest |
tools/ |
Python loader, analysis/figures, satform_ui/ (GUI + WebGL viewer) |
examples/named/ |
Hand-authored real-satellite recipes |
datasets/reference/ |
Curated cited real-spacecraft dimensions (source) |
docs/ |
HDF5_FORMAT.md, CONVERTER.md, VIEWER.md, NAMED_SATELLITES.md, BENCHMARK.md |
release/ |
REGENERATE.md, DATA_RECORDS.md (canonical-release recipe), licences |
Dependencies: Eigen 3.4, HighFive/HDF5, nlohmann-json (via vcpkg).
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=<vcpkg>/scripts/buildsystems/vcpkg.cmake
cmake --build build --config ReleaseCyrillic-path caveat: the HDF5 C runtime cannot open files whose path contains non-ASCII characters. Reading config from a non-ASCII path is fine; only the output directory must be ASCII (e.g.
E:\satform_demo).
$exe = "build/Release/satform.exe"
# Procedural generation (writes .h5 + .obj; add --gltf --urdf for more formats)
& $exe generate --config config --out E:\satform_demo --count 100 --seed 0 --mission leo_eo
# Build a named real satellite from its recipe.
# NOTE --no-collision: a deployed wing's rest hull spans the bus, so the curated
# references are built with the placement gate off (see docs/NAMED_SATELLITES.md).
& $exe build examples/named/recipe_grace_fo.json --config config --out E:\satform_demo\grace.h5 --no-collision
# Convert between formats (HDF5 is the lossless master; conversions warn about the main losses)
& $exe convert E:\satform_demo\grace.h5 E:\satform_demo\grace.glb
# Population statistics + realism report
& $exe stats --config config --count 2000 --seed 12345 --out stats.json$env:SATFORM_WORK_DIR = "C:\satform_ui_work" # ASCII scratch dir (HDF5-safe)
python -m streamlit run tools/satform_ui/app.pyGenerate a model (or pick a named satellite) and inspect it in the embedded
Three.js viewer — mouse-orbit, per-subsystem show/hide, material shading, and
live deployment sliders. three.js is vendored (tools/satform_ui/viewer/vendor/),
so the viewer is fully offline/firewall-safe. See docs/VIEWER.md.
Every change is verified by the test suite:
# One command runs everything:
ctest --test-dir build -C Release --output-on-failure
# ...or the gates on populations:
ctest --test-dir build -C Release -L gate --output-on-failure
build/Release/property_test.exe config 10000 # 9 validators, zero failures
build/Release/statistics_test.exe # realism: 13 of 13 regimes passThe gates measure populations — distributions, feasibility, plausibility. None of them asks whether an individual part is a sound solid, which is how four separate open-mesh defects once reached shipped data. Two tests cover that:
build/Release/geometry_test.exe # primitives: closedness, dimensions, winding
python tools/analysis/mesh_quality.py --generate ./build/Release/satform.exe `
--config config --count 30 --assert-watertightmesh_quality.py also runs against an existing dataset directory, reporting the
watertight fraction and the worst offenders grouped by part name — so a
regression points at a recipe rather than at a model index.
Closed is not the same as free of self-intersection: two closed boxes pushed
into each other and stored as one mesh are closed edge for edge. stats --self-intersect N measures that over the first N generated models, mesh by
mesh — pairs of triangles that cross, pairs that overlap in a common plane, and
any other contact away from a shared vertex or edge — and breaks the count down
by catalog id and by geometry recipe. It is a measurement, not a validator; the
definitions and the tolerance are in src/core/MeshSelfIntersection.h.
build/Release/satform.exe stats --config config --count 150 --seed 4242 --self-intersect 150
build/Release/self_intersection_test.exe # the pair test, known meshes, every primitiveOn that sample all 7,933 component meshes have no two triangles meeting anywhere
but at a vertex or an edge they share, and so do all 182,694 over 300 models of
each of the 13 regimes (seed 12345). On an earlier build, where the two samples
held 8,937 and 170,679 meshes, the measurement first found 1 and 86 that
did not, every one an L-shaped bracket or instrument part:
Primitives::lBracket wrote two boxes into one mesh, and both contained the
corner block. It now meshes the union of the two as one solid.
Projected cross-section is reported as the Cauchy bound S/4, exact for a convex
body and an upper bound for a spacecraft. A BVH ray caster measures the real thing:
build/Release/satform.exe stats --config config --count 200 --seed 4242 --shadow 50Measured by that command the true area is 0.87 × (S/4) at the median (p10
0.79), so the bound overstates the effective cross-section by ~15% typically. The
ratio differs between regimes. Over 40 models of each mission regime at its release
seed, at 256 directions, the median is lowest for deep_space (0.77), servicer
(0.78) and body_mounted (0.79) and highest for megaconstellation (0.94),
spent_stage and geo_comms (0.88), with cubesat at 0.84.
Correctness is pinned by the Cauchy identity itself — for a convex body the
direction-averaged area must equal S/4 exactly, which ray_test checks on a
sphere, a cube and a cylinder.
A model can carry that measurement itself. generate --illuminated-area (and
build --illuminated-area) stores it on the file's root, beside
total_surface_area, as mean_illuminated_area [m²] with the lattice size in
illuminated_area_directions — 256 unless --illuminated-area-dirs D says
otherwise. The flag is off by default and a file written without it has neither
attribute:
from satform_loader import load_model
m = load_model("model_0000.h5")
m.mean_illuminated_area # m², or None if the file was not measured
m.total_surface_area / 4 # the Cauchy bound S/4, for comparisonstats --shadow N --csv per_model.csv writes the measured area of each of the
first N models in a column illuminated_area_m2, with
illuminated_area_to_mass beside it; at --shadow-dirs 256 it is the number
generate stores (stats defaults to 64 directions). projected_area,
area_to_mass and the realism criteria stay defined on S/4.
The same visibility structure turns a mesh into a force model. generate --srp N
appends an optional /srp group: force and torque over N Sun directions, already
divided by the solar pressure (so m² and m³ — multiply by the P your orbit
implies), occlusion included, torque referenced to the centre of mass.
from satform_loader import load_model
m = load_model("model_0000.h5")
F = m.srp.force(4.56e-6) # N at 1 AU, one row per direction
i = m.srp.nearest([0, 0, 1]) # index of the tabulated +Z directionPinned by closed forms, not plausible curves: at normal incidence an absorber gives
|F|/P = A, a mirror 2A, a Lambertian A(1+2/3); off-normal the absorber follows
A cos θ; symmetric plates give identically zero net torque. A 23 t generated model
comes out at 2.3–6.1 × 10⁻⁸ m/s² at 1 AU, the range spacecraft of that class fly.
- The dataset is deposited on Zenodo: https://doi.org/10.5281/zenodo.23892018.
- The canonical release recipe is in
release/REGENERATE.md; the record layout is inrelease/DATA_RECORDS.md.python tools/release/build_final.pyruns the whole recipe, from the build and the tests to the packed deposit. - Python loading (
tools/satform_loader.py):
from satform_loader import load_model, model_to_facets
model = load_model("model_0042.h5")
bus = next(c for c in model.root.children if c.component_type == "bus")
print(bus.catalog_type_id) # bus archetype, e.g. "bus_cube"
facets = model_to_facets(model) # per-facet arrays + subsystem labelsmodel_to_facets returns three distinct label levels, because a segmentation task
needs the right one:
| field | question it answers |
|---|---|
subsystem_id |
semantic: this facet is power |
instance_id |
instance: this facet belongs to the second of the two wings |
part_id |
part: the wing's strut versus its panel |
An instance is one grammar-rule firing, so a three-panel folding wing is one
instance and a truss mast of two dozen struts is one instance — which is what an
instance-segmentation metric has to count. (A launch-adapter ring is one instance
and, being a single mesh, one part; its angular sectors are a collision-only
partition of that mesh, the convex_piece dataset, and are not parts.) catalog_type names the generative
source, so a model can be regrouped by what the catalogue calls a thing rather than
by what it happens to look like.
Frozen splits hold out whole bus archetypes for test:
python tools/make_splits.py dataset -o release/splits.jsonA random split measures almost nothing here — every model comes from one grammar,
so a random test set contains the same archetypes as training. Holding out
morphologies asks the harder question: does a model trained on some spacecraft
shapes transfer to shapes it has never seen? Verified disjoint: on a 180-model
sample, bus_hex and bus_trapezoid appear only in test, every other archetype
only in train/val, zero leakage.
A pose metric is ill-defined on a symmetric object: if a rotation maps the
spacecraft onto itself, an estimate differing from the label by exactly that
rotation is not an error. generate --symmetry writes an optional /symmetry
group; the canonical release recipe enables it.
m = load_model("model_0000.h5")
R = m.symmetry.equivalent_rotations() # (K,3,3) — quotient these out in a metricDetected from geometry, not read off the grammar — and the measurement is why.
The grammar is full of symmetric operators (bilateral_pair, radial_n), which
made "derive it from the rules" look obvious. Measured, 93–100 % of models are
C1: the wings are placed as a mirrored pair, then one antenna or boom breaks the
symmetry of the whole model. Deriving it from the grammar would have declared
almost every two-winged model symmetric.
equivalent_rotations excludes mirrors deliberately: a pose is an element of
SO(3), so an improper transform is not an alternative pose and quotienting by one
would forgive a handedness error.
If you use the SatForm3D engine or the SHAPE dataset, please cite them —
see CITATION.cff. The engine and the dataset are separately
licensed: Apache-2.0 for the code, CC BY 4.0 for the data.
Apache License 2.0 — see LICENSE.
One exception: tools/satform_ui/viewer/vendor/ holds revision 160 of the
Three.js rendering library, which carries its own MIT licence — see
tools/satform_ui/viewer/vendor/LICENSE.