MassCompose
MassCompose takes a mesh and the point masses bolted to it, like a battery, motor, or controller, and gives you one total mass, one center of mass, and one 3×3 inertia tensor about that center of mass.
trimesh already handles the mesh's own properties, so the part this package adds is the parallel-axis composition. The shifts have to come from the system center of mass instead of the origin, since a wrong tensor is still symmetric and positive semi-definite and looks fine until the simulation starts tumbling.
PyPI release
https://pypi.org/project/masscompose/
Demo app
There's a small GUI demo in the github releases page
(https://github.com/Iarush/masscompose) as Demo.zip. Unzip it and run python run.py (or ./run.sh on Mac/Linux), which sets up a local .venv and installs everything the first time. After that you can skip the install checks by launching app.py with the venv's Python directly, .venv\Scripts\python app.py on Windows or .venv/bin/python app.py on Mac/Linux.
Installation
pip install masscompose
It only needs numpy and trimesh. From a checkout, pip install -e ".[dev]" adds pytest.
Usage
import numpy as np
import trimesh
from masscompose import PointMass, compose, load_mesh, to_urdf_inertial
# Stand-in for your own file: a 300 x 200 x 8 mm plate, exported in mm
trimesh.creation.box(extents=[300.0, 200.0, 8.0]).export("chassis.stl")
# PETG at 40% infill. Units are always declared, never guessed
chassis = load_mesh("chassis.stl", mesh_units="mm", density=1240.0, infill=0.4)
payload = [
PointMass([0.000, 0.000, 0.050], mass=0.420, name="battery"),
PointMass([0.080, -0.040, 0.020], mass=0.075, name="esc"),
PointMass([-0.120, 0.000, 0.090], mass=0.031, name="camera"),
]
mp = compose(chassis, payload)
print(f"mass = {mp.mass:.4f} kg")
print(f"com = {np.round(mp.com, 5).tolist()} m")
print(to_urdf_inertial(mp))
mass = 0.7641 kg
com = [0.00298, -0.00393, 0.0331] m
<inertial>
<origin xyz="0.002983980735 -0.00392629044079 0.0330986284159" rpy="0 0 0"/>
<mass value="0.76408"/>
<inertia ixx="0.00139712657604" ixy="0.000231048057795" ixz="0.000290264872788" iyy="0.00320050197129" iyz="-3.92958852476e-05" izz="0.0036070176526"/>
</inertial>
Point mass positions are in meters and share the mesh file's frame after unit conversion. The tensor is about mp.com, which is why the <origin> carries it.
If you only need the mesh
You don't need this package for that, since trimesh does it in three lines:
mesh = trimesh.load("chassis.stl", force="mesh")
mesh.apply_scale(0.001) # your file's units -> meters
mesh.density = 1240.0 # then read mesh.mass, mesh.center_mass, mesh.moment_inertia
Use MassCompose when there are discrete masses to add on top.
Scope
| Does | Doesn't |
|---|---|
| One watertight mesh with a density or known mass | Multi-body assemblies or sub-bodies with their own orientation |
| N point masses at explicit XYZ | Point masses with their own extent |
Required mesh_units ("mm", "cm", "m") |
Guess units from the file |
Effective density through infill |
Per-region or graded density |
| Raises on open meshes and bad inertia | Repair a broken mesh |
URDF <inertial> export |
MJCF, USD, SDF |
Everything is SI on output: kg, m, kg·m².
API
load_mesh(path, *, mesh_units, density=None, mass=None, infill=1.0, allow_open=False) -> MeshBody
Mass properties of one mesh, with inertia about the mesh's own COM. You need exactly one of density (kg/m³) or mass (kg), and infill can't be combined with mass. A non-watertight mesh raises ValueError unless allow_open=True, which turns it into a warning.
compose(body: MeshBody, point_masses: Sequence[PointMass]) -> MassProperties
Total mass, system COM, and the inertia tensor about that COM. An empty list just returns the mesh's own properties, and the output is checked before it's returned.
to_urdf_inertial(mp: MassProperties) -> str
The <inertial> block as a string, ready to paste inside a <link>. The i** attributes are plain matrix entries (ixy is inertia[0, 1]), so there's no sign flip.
PointMass(position: np.ndarray, mass: float, name: str | None = None)
A discrete mass in the mesh's frame. position has to be shape (3,), and a (3, 1) column gets rejected instead of flattened since a transposed vector gives a wrong but plausible answer.
MeshBody(mass, com, inertia) # returned by load_mesh
MassProperties(mass, com, inertia) # returned by compose
Metadata
Release files for masscompose 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| masscompose-0.1.1.tar.gz | 12.6 kB | Without provenance | Details for masscompose-0.1.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| masscompose-0.1.1-py3-none-any.whl | Python 3 | none | any | Details for masscompose-0.1.1-py3-none-any.whl |
Total release size: 28.6 kB
Release files / masscompose-0.1.1.tar.gz
| Download URL | masscompose-0.1.1.tar.gz |
|---|---|
| Size | 12.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
60300a22d71327ef7c395b7707f1692831e8765651aac2c65b5c1be2d2faa0d8
|
|
BLAKE2b-256 checksum How to use checksums |
14c06ea55be5d72c86f6252a0e20daa4775ebd6c32a72ac59cfd51a7a243768f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / masscompose-0.1.1-py3-none-any.whl
| Download URL | masscompose-0.1.1-py3-none-any.whl |
|---|---|
| Size | 16.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
403520149d5ebf1b33750cc9230015fe8b32a59066fbbc09f1917c3cb5f69bb2
|
|
BLAKE2b-256 checksum How to use checksums |
6684b1acb2b31194a6a12bae76000c1034f6754412bd04286c7561265dbf1e73
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Security for masscompose 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.