Contributing To Trimesh¶
Pull requests are always super welcome! Trimesh is a relatively small open source project and really benefits from the bugfixes, features, and other stuff the 100+ contributors have PR’d, so thanks!
Developer Quick Start¶
Here’s how I set up a new environment and write functions. It’s not necessary to do it this way but it does make some things easier! If you don’t have a “virtual environment” solution there’s plenty of ways to do this (poetry, pipenv, conda, etc.) but I just use the venv
module built into the standard library:
# create the venv
python -m venv ~/venv
# on linux this will use this venv
# every time you open a new terminal
echo "source ~/venv/bin/activate" >> ~/.bashrc
Then when you open a new terminal you can verify it got the right environment with which
:
mikedh@orion:trimesh$ which python
/home/mikedh/venv/bin/python
mikedh@orion:trimesh$ which pip
/home/mikedh/venv/bin/pip
If you’re planning on editing trimesh you might want to fork it via the Github interface, then install it via an editable pip install:
# you probably want to clone your fork
git clone git@github.com:mikedh/trimesh.git
# do an editable install so you can experiment
cd trimesh
pip install -e .[easy,test]
I pretty much always start with an interactive terminal (i.e. a “REPL”) inside a stub function:
import trimesh
import numpy as np
def fancy_function(blah):
if blah.shape != (3, 3):
raise ValueError('this input was goofy!')
# do some obvious operations and whatnot
dots = np.dot(blah, [1,2,3])
# get a REPL inside my function so I can write each line
# with the context of the function, copy paste the lines
# in and at the end return the value and remove the embed
from IPython import embed
embed()
if __name__ == '__main__':
# print out all the debug messages so we can see
# if there's something going on we need to look at
trimesh.util.attach_to_log()
# I like pyinstrument as it's a relatively low-overhead sampling
# profiler and has nice looking nested print statements compared
# to cProfile or others.
import pyinstrument
data = np.random.random((3, 3))
with pyinstrument.Profiler() as pr:
result = fancy_function(data)
pr.print()
When you remove the embed and see the profile result you can then tweak the lines that are slow before finishing the function.
Automatic Formatting¶
Trimesh uses ruff
for both linting and formatting which is configured in pyproject.toml
, you can run with:
ruff check --fix
ruff format
Docstrings¶
Trimesh uses the Sphinx Numpy-style docstrings which get parsed into the API reference page.
General Tips¶
Python can be fast but only when you use it as little as possible. In general, if you ever have a block which loops through faces and vertices it will be basically unusable with even moderately sized meshes. All operations on face or vertex arrays should be vectorized numpy operations unless absolutely unavoidable. Profiling helps figure out what is slow, but some general advice:
Helpful¶
Run your test script with
ipython -i newstuff.py
and profile with magic, i.e.%timeit var.split()
Use
np.fromstring
,np.frombuffer
Use
str.split
ornp.fromstring
:
In [6]: %timeit np.array(text.split(), dtype=np.float64)
1000 loops, best of 3: 209 µs per loop
In [7]: %timeit np.fromstring(text, sep='\n', dtype=np.float64)
10000 loops, best of 3: 139 µs per loop
Use giant format strings rather than looping, appending, or even iterator joining:
In [14]: array = np.random.random((10000,3))
In [15]: %timeit '\n'.join('{}/{}/{}'.format(*row) for row in array)
10 loops, best of 3: 60.3 ms per loop
In [16]: %timeit ('{}/{}/{}\n' * len(array))[:-1].format(*array.flatten())
10 loops, best of 3: 34.3 ms per loop
Sometimes you can use sparse matrices to replace a loop and get a huge speedup. Here’s the same algorithm implemented two ways, looping and sparse dot products.
In tight loops,
array.sum(axis=1)
often pops up as the slowest thing. This can be replaced with a dot product of ones, which are very optimized can be substantially faster:
In [1]: import numpy as np
In [2]: a = np.random.random((10000, 3))
In [3]: %timeit a.sum(axis=1)
10000 loops, best of 3: 157 µs per loop
In [4]: %timeit np.dot(a, [1,1,1])
10000 loops, best of 3: 25.8 µs per loop
If you can use it,
np.concatenate
is usually faster thannp.vstack
,np.append
, ornp.column_stack
In [3]: seq = [np.random.random((int(np.random.random() * 1000), 3)) for i in range(1000)]
In [7]: %timeit np.vstack(seq)
100 loops, best of 3: 3.48 ms per loop
In [8]: %timeit np.concatenate(seq)
100 loops, best of 3: 2.33 ms per loop
Sometimes
np.bincount
can be used instead ofnp.unique
for a substantial speedup:
In [45]: a = (np.random.random(1000) * 1000).astype(int)
In [46]: set(np.where(np.bincount(a).astype(bool))[0]) == set(np.unique(a))
Out[46]: True
In [47]: %timeit np.where(np.bincount(a).astype(bool))[0]
100000 loops, best of 3: 5.81 µs per loop
In [48]: %timeit np.unique(a)
10000 loops, best of 3: 31.8 µs per loop
Try To Avoid¶
Looping in general, and especially looping on arrays that could have many elements(i.e. vertices and faces). The loop overhead is very high in Python. If necessary to loop you may find that list comprehensions are quite a bit faster (though definitely profile first!) probably for scoping reasons.
Boolean operations (i.e. intersection, difference, union) on meshes may seem like the answer, but they are nearly always flaky and slow. The best answer is usually to restructure your problem to use some form of vector checks if possible (i.e. dot products, ray tests, etc). Look at
trimesh.intersections
for an example of a problem that could have used a boolean operation but didn’t.