Pull requests are appreciated!
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! Start with installing some stuff:
# I create a virtul env I enter every time I open a
# terminal. There are plenty of alternative
# options like poetry, pipenv, conda, etc but for simple
# pip-installing-to-my-home-dir needs this works fine
python -m venv ~/myenv
# enter the venv every time you open a new terminal
echo "source ~/myenv/bin/activate" >> ~/.bashrc
# pip install in our venv
# no-implicit-concat adds a rule which disallows strings on
# different lines with no comma being concatenated as this is
# pretty much always a bug.
pip install autopep8 flake8 flake8-no-implicit-concat codespell pyinstrument ipython
I pretty much always start with an interactive terminal (i.e. a "REPL") inside a stub function:
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__':
# 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.
Before PR'ing I run some auto-formatting rules which are available in setup.py
:
# run autopep8 and yell at you about any flake8 rule violations
python setup.py --format
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:
- 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
- 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.