MassCompose
Combine a mesh's inertia with the point masses bolted to it — battery, motor, controller — into one total mass, one centre of mass, and one 3×3 inertia tensor about that centre of mass.
trimesh already gives you the mesh's own properties. The part that gets
written wrong by hand is the parallel-axis composition that folds discrete
masses in with it: the shifts have to be taken from the system centre of
mass, not from the origin, and the resulting tensor is symmetric and
positive semi-definite either way, so a wrong one looks entirely plausible
until the thing it describes tumbles in simulation.
Installation
pip install masscompose
Runtime dependencies are numpy and trimesh, and nothing else. 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, so this example runs as written:
# a 300 x 200 x 8 mm chassis plate, exported in millimetres.
trimesh.creation.box(extents=[300.0, 200.0, 8.0]).export("chassis.stl")
# PETG at 40% infill. Units are 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 metres, in the same frame as the mesh file's
coordinates after unit conversion. The tensor is about mp.com, which is why
the <origin> carries it — the two have to agree or the block is wrong.
If you only need mesh inertia
You do not need this package. trimesh does it in three lines:
mesh = trimesh.load("chassis.stl", force="mesh")
mesh.apply_scale(0.001) # your file's units -> metres
mesh.density = 1240.0 # then read mesh.mass, mesh.center_mass, mesh.moment_inertia
Reach for MassCompose when there are discrete masses to fold in on top of that.
Scope
| Does | Does not |
|---|---|
| One watertight mesh, given a density or a known mass | Multi-body assemblies, or sub-bodies with their own orientation |
| N point masses at explicit XYZ | Point masses with extent of their own |
| Parallel-axis composition about the system centre of mass | Dynamics, simulation, or anything time-dependent |
Mandatory mesh_units — "mm", "cm", "m" |
Guessing units from the file |
Effective density via infill for printed parts |
Per-region or graded density |
| Watertightness, volume, and density checks that raise | Repairing a broken mesh |
| Post-condition checks: symmetry, positive semi-definiteness, triangle inequality | Silently returning a plausible-looking wrong tensor |
URDF <inertial> export |
MJCF, USD, SDF |
| SI throughout — kg, m, kg·m² | Any other unit system on output |
API
The importable surface is these six names and nothing else.
load_mesh(path, *, mesh_units, density=None, mass=None, infill=1.0, allow_open=False) -> MeshBody
Mass properties of one mesh file, in SI, with inertia about the mesh's own
COM. Exactly one of density (kg/m³) or mass (kg) is required. infill
scales density for printed parts and cannot be combined with mass. Raises
ValueError on a non-watertight mesh unless allow_open=True downgrades it
to a warning.
compose(body: MeshBody, point_masses: Sequence[PointMass]) -> MassProperties
Total mass, system COM, and the inertia tensor about that COM. An empty
point_masses returns the mesh's own properties. Validates its own output.
to_urdf_inertial(mp: MassProperties) -> str
The <inertial> block as a string, ready to paste inside a <link>. The six
i** attributes are matrix entries, so ixy is inertia[0, 1] — the standard
physics product of inertia, no sign flip.
PointMass(position: np.ndarray, mass: float, name: str | None = None)
A discrete mass, in the mesh's frame. position must be shape (3,) exactly —
a (3, 1) column is rejected rather than flattened, because a transposed
vector reaches the parallel-axis step and comes out wrong but plausible.
MeshBody(mass: float, com: np.ndarray, inertia: np.ndarray)
What load_mesh returns: inertia about com, in SI.
MassProperties(mass: float, com: np.ndarray, inertia: np.ndarray)
What compose returns: same three fields, same convention.
Tests
pytest
Mesh properties are checked against closed-form values for a sphere, box, and cylinder; composition against hand-computed parallel-axis results, including the off-diagonal signs; and the URDF block by round-tripping the XML back to a 3×3.
Metadata
Release files for masscompose 0.1.0
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.0.tar.gz | 12.5 kB | Without provenance | Details for masscompose-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| masscompose-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for masscompose-0.1.0-py3-none-any.whl |
Total release size: 28.7 kB
Release files / masscompose-0.1.0.tar.gz
| Download URL | masscompose-0.1.0.tar.gz |
|---|---|
| Size | 12.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
72fcc079e531365f3482f7994f53bfda5b35c436f422a9c47b5f6bbae49d1487
|
|
BLAKE2b-256 checksum How to use checksums |
400c9a9488948e7551c8db9e7ed3c33ee8883e0ccb2b308a58584f1d9702b425
|
| 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.0-py3-none-any.whl
| Download URL | masscompose-0.1.0-py3-none-any.whl |
|---|---|
| Size | 16.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
af0d67f826e8ab8de5e6826112609c7fa15eb90619428f2a9648b39806c04b41
|
|
BLAKE2b-256 checksum How to use checksums |
50a02ba44ce22e3ae4ea72685e9349e5a8c58c76e1723f13a32c1da3b50407de
|
| 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.0
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.