Skip to main content

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)

Source distribution for masscompose 0.1.0
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)

Table of built distributions (wheels) for masscompose 0.1.0
File Interpreter ABI Platform Actions
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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page