Repository: mikedh/trimesh
Pull request #1856 (2023-02-23 15:54:18)

Cache calculation of children
When using `successors` across all nodes of a large model, it's call to `children` appears to be the main computational bottleneck.
These changes cache the result of `children` (in the same structure as paths are cached).

Test script
```
import numpy as np
from trimesh.scene.transforms import EnforcedForest

def random_chr():
    return chr(ord('a') + int(round(np.random.random() * 25)))

if __name__ == '__main__':
    graph = EnforcedForest()
    for i in range(100):
        level1 = 'a_{}'.format(i)
        graph.add_edge('base_frame', level1)
        for j in range(50):
            level2 = 'b_{}_{}'.format(i, j)
            graph.add_edge(level1, level2)

    for n in graph.nodes:
        succ = graph.successors(n)
```
Profiling results before change
```
  _     ._   __/__   _ _  _  _ _/_   Recorded: 10:48:07  Samples:  4189
 /_//_/// /_\ / //_// / //_'/ //     Duration: 4.219     CPU time: 7.341
/   _/                      v4.4.0

Program: test.py

4.219 <module>  test.py:1
├─ 3.868 EnforcedForest.successors  trimesh/scene/transforms.py:734
│  └─ 3.862 EnforcedForest.children  trimesh/scene/transforms.py:715
│     ├─ 3.807 <listcomp>  trimesh/scene/transforms.py:728
│     │  ├─ 2.831 [self]  None
```
Profiling results after change
```
  _     ._   __/__   _ _  _  _ _/_   Recorded: 10:48:19  Samples:  280
 /_//_/// /_\ / //_// / //_'/ //     Duration: 0.312     CPU time: 3.389
/   _/                      v4.4.0

Program: test.py

0.311 <module>  test.py:1
├─ 0.242 <module>  trimesh/__init__.py:1
│  ├─ 0.225 <module>  trimesh/base.py:1
│  │  ├─ 0.128 <module>  trimesh/ray/__init__.py:1
│  │  │  └─ 0.127 <module>  trimesh/ray/ray_triangle.py:1
│  │  │     └─ 0.127 <module>  trimesh/ray/ray_util.py:1
│  │  │        ├─ 0.121 <module>  trimesh/bounds.py:1
```


Patch:
diff --git a/README.md b/README.md
index be65d759..5a2fd164 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 [![trimesh](https://trimsh.org/images/logotype-a.svg)](http://trimsh.org)
 
 -----------
-[![Github Actions](https://github.com/mikedh/trimesh/workflows/Release%20Trimesh/badge.svg)](https://github.com/mikedh/trimesh/actions)  [![PyPI version](https://badge.fury.io/py/trimesh.svg)](https://badge.fury.io/py/trimesh) [![codecov](https://codecov.io/gh/mikedh/trimesh/branch/main/graph/badge.svg?token=4PVRQXyl2h)](https://codecov.io/gh/mikedh/trimesh)  [![Docker Image Version (latest by date)](https://img.shields.io/docker/v/trimesh/trimesh?label=docker)](https://hub.docker.com/repository/docker/trimesh/trimesh/)
+[![Github Actions](https://github.com/mikedh/trimesh/workflows/Release%20Trimesh/badge.svg)](https://github.com/mikedh/trimesh/actions)  [![PyPI version](https://badge.fury.io/py/trimesh.svg)](https://badge.fury.io/py/trimesh) [![codecov](https://codecov.io/gh/mikedh/trimesh/branch/main/graph/badge.svg?token=4PVRQXyl2h)](https://codecov.io/gh/mikedh/trimesh)
 
 
 Trimesh is a pure Python (2.7-3.5+) library for loading and using [triangular meshes](https://en.wikipedia.org/wiki/Triangle_mesh) with an emphasis on watertight surfaces. The goal of the library is to provide a full featured and well tested Trimesh object which allows for easy manipulation and analysis, in the style of the Polygon object in the [Shapely library](https://github.com/Toblerity/Shapely).
diff --git a/examples/sinter.py b/examples/sinter.py
index a5c57918..c534f8ce 100644
--- a/examples/sinter.py
+++ b/examples/sinter.py
@@ -17,7 +17,7 @@ models = os.path.abspath(os.path.join(
     os.path.expanduser(os.path.dirname(__file__)), '..', 'models'))
 
 
-def collect_meshes(count=None, max_size=20.0):
+def collect_meshes(count=None):
     """
     Collect single body watertight meshes from our
     models folder.
@@ -41,8 +41,7 @@ def collect_meshes(count=None, max_size=20.0):
             pass
         for ori in scene.geometry.values():
             if (not isinstance(ori, trimesh.Trimesh) or
-                    not ori.is_watertight or ori.volume < 0.001 or
-                    ori.extents.max() > max_size):
+                    not ori.is_watertight or ori.volume < 0.001):
                 continue
 
             # split into single body meshes
@@ -59,17 +58,16 @@ def collect_meshes(count=None, max_size=20.0):
 
 if __name__ == '__main__':
 
-    size = 10.0
     # get some sample data
-    meshes = collect_meshes(max_size=size)
+    meshes = collect_meshes(count=100)
 
     print('loaded {} meshes'.format(len(meshes)))
 
     # place the meshes into the volume
     with Profiler() as P:
         placed, transforms, consume = packing.meshes(
-            meshes, size=[size] * 3, spacing=0.1)
-    P.print(show_all=True)
+            meshes, spacing=0.05)
+    P.print()
 
     # none of the placed meshes should have overlapping AABB
     assert not packing.bounds_overlap([i.bounds for i in placed])
@@ -85,3 +83,6 @@ if __name__ == '__main__':
         plane_origin=concat.bounds[0],
         plane_normal=[0, 0, 1],
         heights=np.linspace(0.0, 10.0, 100))
+
+    # show a cross section in the middle
+    sections[50].show()
diff --git a/trimesh/comparison.py b/trimesh/comparison.py
index c29cec9a..7cbbce5a 100644
--- a/trimesh/comparison.py
+++ b/trimesh/comparison.py
@@ -15,7 +15,7 @@ from .constants import tol
 # how many significant figures to use for each
 # field of the identifier based on hand-tuning
 id_sigfig = np.array(
-    [4,   # area
+    [4,  # area
      10,  # euler number
      5,  # area/volume ratio
      2,  # convex/mesh area ratio
diff --git a/trimesh/exchange/dae.py b/trimesh/exchange/dae.py
index d498fe97..eaf995ce 100644
--- a/trimesh/exchange/dae.py
+++ b/trimesh/exchange/dae.py
@@ -7,12 +7,8 @@ import numpy as np
 from .. import util
 from .. import visual
 
-from ..util import unique_name
 from ..constants import log
 
-_EYE = np.eye(4)
-_EYE.flags.writeable = False
-
 
 def load_collada(file_obj,
                  resolver=None,
@@ -64,16 +60,13 @@ def load_collada(file_obj,
 
     # name : kwargs
     meshes = {}
-    # increments to enable `unique_name` to avoid n^2 behavior
-    meshes_count = {}
     # list of dict
     graph = []
     for node in c.scene.nodes:
         _parse_node(node=node,
-                    parent_matrix=_EYE,
+                    parent_matrix=np.eye(4),
                     material_map=material_map,
                     meshes=meshes,
-                    meshes_count=meshes_count,
                     graph=graph,
                     resolver=resolver)
 
@@ -171,7 +164,6 @@ def _parse_node(node,
                 parent_matrix,
                 material_map,
                 meshes,
-                meshes_count,
                 graph,
                 resolver=None):
     """
@@ -239,19 +231,17 @@ def _parse_node(node,
                     vis = visual.texture.TextureVisuals(
                         uv=uv, material=material)
 
-                geom_name = unique_name(geometry.id,
-                                        contains=meshes,
-                                        counts=meshes_count)
-                meshes[geom_name] = {
+                primid = u'{}.{}'.format(geometry.id, i)
+                meshes[primid] = {
                     'vertices': vertices,
                     'faces': faces,
                     'vertex_normals': normals,
                     'vertex_colors': colors,
                     'visual': vis}
 
-                graph.append({'frame_to': geom_name,
+                graph.append({'frame_to': primid,
                               'matrix': parent_matrix,
-                              'geometry': geom_name})
+                              'geometry': primid})
 
     # recurse down tree for nodes with children
     elif isinstance(node, collada.scene.Node):
@@ -265,7 +255,6 @@ def _parse_node(node,
                     parent_matrix=matrix,
                     material_map=material_map,
                     meshes=meshes,
-                    meshes_count=meshes_count,
                     graph=graph,
                     resolver=resolver)
 
diff --git a/trimesh/exchange/gltf.py b/trimesh/exchange/gltf.py
index ea0d2a46..f46b9a7f 100644
--- a/trimesh/exchange/gltf.py
+++ b/trimesh/exchange/gltf.py
@@ -1296,6 +1296,7 @@ def _read_buffers(header,
                 start = 0
             end = start + view["byteLength"]
             views[i] = buffers[view["buffer"]][start:end]
+
             assert len(views[i]) == view["byteLength"]
 
         # load data from buffers into numpy arrays
@@ -1307,7 +1308,6 @@ def _read_buffers(header,
             # what is the datatype
             dtype = np.dtype(_dtypes[a["componentType"]])
             # basically how many columns
-            # for types like (4, 4)
             per_item = _shapes[a["type"]]
             # use reported count to generate shape
             shape = np.append(count, per_item)
@@ -1336,7 +1336,7 @@ def _read_buffers(header,
                     # the total block we're looking at
                     length = count * stride
                     # we want to get the bytes for every row
-                    per_row = per_count * dtype.itemsize
+                    per_row = per_item * dtype.itemsize
                     # we have to offset the (already offset) buffer
                     # and then pull chunks per-stride
                     # do as a list comprehension as the numpy
@@ -1365,10 +1365,7 @@ def _read_buffers(header,
     # load data from accessors into Trimesh objects
     meshes = collections.OrderedDict()
 
-    # keep track of how many times each name has been attempted to
-    # be inserted to avoid a potentially slow search through our
-    # dict of names
-    name_counts = {}
+    names_original = collections.defaultdict(list)
 
     for index, m in enumerate(header.get("meshes", [])):
 
@@ -1392,8 +1389,9 @@ def _read_buffers(header,
                 attr = p['attributes']
                 # create a unique mesh name per- primitive
                 name = m.get('name', 'GLTF')
+                names_original[index].append(name)
                 # make name unique across multiple meshes
-                name = unique_name(name, meshes, counts=name_counts)
+                name = unique_name(name, meshes)
 
                 if mode == _GL_LINES:
                     # load GL_LINES into a Path object
@@ -1465,6 +1463,7 @@ def _read_buffers(header,
                     # each primitive gets it's own Trimesh object
                     if len(m["primitives"]) > 1:
                         kwargs['metadata']['from_gltf_primitive'] = True
+                        name = unique_name(name, meshes)
                     else:
                         kwargs['metadata']['from_gltf_primitive'] = False
 
@@ -1546,13 +1545,9 @@ def _read_buffers(header,
     # to a dict comprehension as it will be checking
     # the mutated dict in every loop
     name_index = {}
-    name_counts = {}
     for i, n in enumerate(nodes):
         name_index[unique_name(
-            n.get('name', str(i)),
-            name_index,
-            counts=name_counts
-        )] = i
+            n.get('name', str(i)), name_index)] = i
     # invert the dict so we can look up by index
     # node index (int) : name (str)
     names = {v: k for k, v in name_index.items()}
diff --git a/trimesh/exchange/stl.py b/trimesh/exchange/stl.py
index 813ed7e4..4ff5018a 100644
--- a/trimesh/exchange/stl.py
+++ b/trimesh/exchange/stl.py
@@ -17,7 +17,7 @@ _stl_dtype = np.dtype([('normals', '<f4', (3)),
                        ('attributes', '<u2')])
 # define a numpy datatype for the header of a binary STL file
 _stl_dtype_header = np.dtype([('header', np.void, 80),
-                              ('face_count', '<u4')])
+                              ('face_count', '<i4')])
 
 
 def load_stl(file_obj, **kwargs):
diff --git a/trimesh/path/packing.py b/trimesh/path/packing.py
index e2138da0..5709c7ea 100644
--- a/trimesh/path/packing.py
+++ b/trimesh/path/packing.py
@@ -71,6 +71,7 @@ class RectangleBin:
           Position of insertion in the tree or None
           if the insertion was unsuccessful.
         """
+
         for child in self.child:
             # try inserting into child cells
             attempt = child.insert(size=size, rotate=rotate)
diff --git a/trimesh/path/polygons.py b/trimesh/path/polygons.py
index 511d4ed7..4ec5c9f4 100644
--- a/trimesh/path/polygons.py
+++ b/trimesh/path/polygons.py
@@ -885,91 +885,39 @@ def projected(mesh,
     return polygon
 
 
-def second_moments(polygon, centered=False):
+def second_moment(coords):
     """
-    Calculate the second moments of area of a polygon
+    Calculate the second moment of area of a polygon
     from the boundary.
 
     Parameters
     ------------
-    polygon : shapely.geometry.Polygon
+    coords : (n, 2) float or Polygon
       Closed polygon.
-    centered: bool
-      Get second moments for a frame with origin at the centroid
-      and perform a principal axis transformation.
 
     Returns
     ----------
     moments : (3,) float
-      The values of `[Ixx, Iyy, Ixy]`
-    principal_moments : (2,) float
-      Principal second moments of inertia: `[Imax, Imin]`
-      Only returned if `centered`.
-    alpha : float
-      Angle by which the polygon needs to be rotated, so the
-      principal axis align with the X and Y axis.
-      Only returned if `centered`.
-    transform : (3, 3) float
-      Transformation matrix which rotates the polygon by alpha.
-      Only returned if `centered`.
+      The values of `[Ix, Iy, Ixy]`
     """
+    if hasattr(coords, 'exterior'):
+        # if we have been passed a shapely.geometry.Polygon
+        exterior = second_moment(np.array(coords.exterior.coords))
+        interiors = np.sum([second_moment(np.array(i.coords))
+                            for i in coords.interiors],
+                           axis=0)
+        return exterior - interiors
 
-    transform = np.eye(3)
-    if centered:
-        # calculate centroid and move polygon
-        transform[:2, 2] = - np.array(polygon.centroid.coords)
-        polygon = transform_polygon(polygon, transform)
-
-    # start with the exterior
-    coords = np.array(polygon.exterior.coords)
+    coords = np.asanyarray(coords, dtype=np.float64)
     # shorthand the coordinates
     x1, y1 = np.vstack((coords[-1], coords[:-1])).T
     x2, y2 = coords.T
     # do vectorized operations
     v = x1 * y2 - x2 * y1
-    Ixx = np.sum(v * (y1 * y1 + y1 * y2 + y2 * y2)) / 12.0
-    Iyy = np.sum(v * (x1 * x1 + x1 * x2 + x2 * x2)) / 12.0
-    Ixy = np.sum(v * (x1 * y2 + 2 * x1 * y1 + 2 * x2 * y2 + x2 * y1)) / 24.0
-
-    for interior in polygon.interiors:
-        coords = np.array(interior.coords)
-        # shorthand the coordinates
-        x1, y1 = np.vstack((coords[-1], coords[:-1])).T
-        x2, y2 = coords.T
-        # do vectorized operations
-        v = x1 * y2 - x2 * y1
-        Ixx -= np.sum(v * (y1 * y1 + y1 * y2 + y2 * y2)) / 12.0
-        Iyy -= np.sum(v * (x1 * x1 + x1 * x2 + x2 * x2)) / 12.0
-        Ixy -= np.sum(v * (x1 * y2 + 2 * x1 * y1 +
-                      2 * x2 * y2 + x2 * y1)) / 24.0
-
-    moments = [Ixx, Iyy, Ixy]
-
-    if not centered:
-        return moments
-
-    # get the principal moments
-    root = np.sqrt(((Iyy - Ixx) / 2.0)**2 + Ixy**2)
-    Imax = (Ixx + Iyy) / 2.0 + root
-    Imin = (Ixx + Iyy) / 2.0 - root
-    principal_moments = [Imax, Imin]
-
-    # do the principal axis transform
-    if np.isclose(Ixy, 0.0, atol=1e-12):
-        alpha = 0
-    elif np.isclose(Ixx, Iyy):
-        # prevent division by 0
-        alpha = 0.25 * np.pi
-    else:
-        alpha = 0.5 * np.arctan(2.0 * Ixy / (Ixx - Iyy))
-
-    # construct transformation matrix
-    cos_alpha = np.cos(alpha)
-    sin_alpha = np.sin(alpha)
-
-    transform[0, 0] = cos_alpha
-    transform[1, 1] = cos_alpha
-    transform[0, 1] = - sin_alpha
-    transform[1, 0] = sin_alpha
-
-    return moments, principal_moments, alpha, transform
+    Ix = v * (y1 * y1 + y1 * y2 + y2 * y2)
+    Iy = v * (x1 * x1 + x1 * x2 + x2 * x2)
+    Ixy = v * (x1 * y2 + 2 * x1 * y1 + 2 * x2 * y2 + x2 * y1)
+    # divide by constants and return
+    return np.array([Ix.sum() / 12.0,
+                     Iy.sum() / 12.0,
+                     Ixy.sum() / 24.0], dtype=np.float64)
diff --git a/trimesh/resolvers.py b/trimesh/resolvers.py
index 09ead18f..3d116dd7 100644
--- a/trimesh/resolvers.py
+++ b/trimesh/resolvers.py
@@ -17,10 +17,10 @@ from . import caching
 # URL parsing for remote resources via WebResolver
 try:
     # Python 3
-    from urllib.parse import urlparse
+    from urllib.parse import urlparse, urljoin
 except ImportError:
     # Python 2
-    from urlparse import urlparse
+    from urlparse import urlparse, urljoin
 
 
 class Resolver(util.ABC):
@@ -328,23 +328,18 @@ class WebResolver(Resolver):
 
         # if the last item in the url path is a filename
         # move up a "directory" for the base path
-        if len(split) == 0:
-            path = ''
-        elif '.' in split[-1]:
+        if len(split) > 0 and '.' in split[-1]:
             # clip off last item
             path = '/'.join(split[:-1])
         else:
             # recombine into string ignoring any double slashes
             path = '/'.join(split)
-        self.base_url = '/'.join(i for i in [
-            parsed.scheme + ':/',
-            parsed.netloc.strip('/'),
-            path.strip('/')] if len(i) > 0) + '/'
 
-        # our string handling should have never inserted double slashes
-        assert '//' not in self.base_url[len(parsed.scheme) + 3:]
-        # we should always have ended with a single slash
-        assert self.base_url.endswith('/')
+        # store the base url
+        self.base_url = '{scheme}://{netloc}/{path}'.format(
+            scheme=parsed.scheme,
+            netloc=parsed.netloc,
+            path=path)
 
     def get(self, name):
         """
@@ -361,17 +356,15 @@ class WebResolver(Resolver):
         # remove leading and trailing whitespace
         name = name.strip()
         # fetch the data from the remote url
-
-        # base url has been carefully formatted
-        url = self.base_url + name
-
-        response = requests.get(url)
+        response = requests.get(urljoin(
+            self.base_url, name))
 
         if response.status_code != 200:
             # try to strip off filesystem crap
             if name.startswith('./'):
                 name = name[2:]
-            response = requests.get(self.base_url + name)
+            response = requests.get(urljoin(
+                self.base_url, name))
 
         if response.status_code == '404':
             raise ValueError(response.content)
@@ -394,7 +387,8 @@ class WebResolver(Resolver):
           With sub-url: `https://example.com/{namespace}`
         """
         # join the base url and the namespace
-        return WebResolver(url=self.base_url + namespace)
+        return WebResolver(url=urljoin(
+            self.base_url, namespace))
 
     def write(self, key, value):
         raise NotImplementedError("can't write to remote")
diff --git a/trimesh/scene/transforms.py b/trimesh/scene/transforms.py
index 1a5c9f5c..9aa2b280 100644
--- a/trimesh/scene/transforms.py
+++ b/trimesh/scene/transforms.py
@@ -722,14 +722,17 @@ class EnforcedForest(object):
         children : dict
           Keyed {node : [child, child, ...]}
         """
+        if 'children' in self._cache:
+            return self._cache['children']
         child = collections.defaultdict(list)
         # append children to parent references
         # skip self-references to avoid a node loop
         [child[v].append(u) for u, v in
          self.parents.items() if u != v]
 
-        # return as a vanilla dict
-        return dict(child)
+        # cache and return as a vanilla dict
+        self._cache['children'] = dict(child)
+        return self._cache['children']
 
     def successors(self, node):
         """
diff --git a/trimesh/util.py b/trimesh/util.py
index 76108058..302f8032 100644
--- a/trimesh/util.py
+++ b/trimesh/util.py
@@ -2360,7 +2360,7 @@ def to_ascii(text):
     return str(text)
 
 
-def is_ccw(points, return_all=False):
+def is_ccw(points):
     """
     Check if connected 2D points are counterclockwise.
 
@@ -2368,41 +2368,28 @@ def is_ccw(points, return_all=False):
     -----------
     points : (n, 2) float
       Connected points on a plane
-    return_all : bool
-      Return polygon area and centroid or just counter-clockwise.
 
     Returns
     ----------
     ccw : bool
       True if points are counter-clockwise
-    area : float
-      Only returned if `return_centroid`
-    centroid : (2,) float
-      Centroid of the polygon.
     """
-    points = np.array(points, dtype=np.float64)
-
-    if len(points.shape) != 2 or points.shape[1] != 2:
-        raise ValueError('only defined for `(n, 2)` points')
-
-    # the "shoelace formula"
-    product = np.subtract(*(points[:-1, [1, 0]] * points[1:]).T)
-    # the area of the polygon
-    area = product.sum() / 2.0
-    # check the sign of the area
-    ccw = area < 0.0
-
-    if not return_all:
-        return ccw
+    points = np.asanyarray(points, dtype=np.float64)
 
-    # the centroid of the polygon uses the same formula
-    centroid = ((points[:-1] + points[1:]) *
-                product.reshape((-1, 1))).sum(axis=0) / (6.0 * area)
+    if (len(points.shape) != 2 or points.shape[1] != 2):
+        raise ValueError('CCW is only defined for 2D')
+    xd = np.diff(points[:, 0])
+    # sum along axis=1 with a dot product
+    yd = np.dot(np.column_stack((
+        points[:, 1],
+        points[:, 1])).reshape(-1)[1:-1].reshape((-1, 2)), [1, 1])
+    area = np.sum(xd * yd) * .5
+    ccw = area < 0
 
-    return ccw, area, centroid
+    return ccw
 
 
-def unique_name(start, contains, counts=None):
+def unique_name(start, contains):
     """
     Deterministically generate a unique name not
     contained in a dict, set or other grouping with
@@ -2415,13 +2402,6 @@ def unique_name(start, contains, counts=None):
       Initial guess for name.
     contains : dict, set, or list
       Bundle of existing names we can *not* use.
-    counts : None or dict
-      Maps name starts encountered before to increments in
-      order to speed up finding a unique name as otherwise
-      it potentially has to iterate through all of contains.
-      Should map to "how many times has this `start`
-      been attempted, i.e. `counts[start]: int`.
-      Note that this *will be mutated* in-place by this function!
 
     Returns
     ---------
@@ -2434,16 +2414,13 @@ def unique_name(start, contains, counts=None):
             start not in contains):
         return start
 
-    # start checking with zero index unless found
-    if counts is None:
-        increment = 0
-    else:
-        increment = counts.get(start, 0)
+    # start checking with zero index
+    increment = 0
     if start is not None and len(start) > 0:
         formatter = start + '_{}'
         # split by our delimiter once
         split = start.rsplit('_', 1)
-        if len(split) == 2 and increment == 0:
+        if len(split) == 2:
             try:
                 # start incrementing from the existing
                 # trailing value
@@ -2460,8 +2437,6 @@ def unique_name(start, contains, counts=None):
     for i in range(increment + 1, 2 + increment + len(contains)):
         check = formatter.format(i)
         if check not in contains:
-            if counts is not None:
-                counts[start] = i
             return check
 
     # this should really never happen since we looped
diff --git a/trimesh/version.py b/trimesh/version.py
index 69dcfd87..88d90c3e 100644
--- a/trimesh/version.py
+++ b/trimesh/version.py
@@ -1,4 +1,4 @@
-__version__ = '3.20.1'
+__version__ = '3.20.0'
 
 if __name__ == '__main__':
     # print version if run directly i.e. in a CI script


Test patch:
diff --git a/tests/generic.py b/tests/generic.py
index 6c22760b..76a83aa4 100644
--- a/tests/generic.py
+++ b/tests/generic.py
@@ -26,8 +26,6 @@ import numpy as np
 
 import trimesh
 
-from uuid import uuid4
-
 from trimesh.base import Trimesh
 from trimesh.constants import tol, tol_path
 from collections import deque
@@ -174,48 +172,14 @@ io_wrap = trimesh.util.wrap_as_stream
 
 def random(*args, **kwargs):
     """
-    A random function always seeded from the same value so tests
-    can use random data but they execute mostly deterministically
-    in a large test matrix.
+    A random function always seeded from the same value.
 
-    Drop-in replacement for `np.random.random(*args, **kwargs)`
+    Replaces: np.random.random(*args, **kwargs)
     """
     state = np.random.RandomState(seed=1)
     return state.random_sample(*args, **kwargs)
 
 
-def random_transforms(count, translate=1000):
-    """
-    Deterministic generation of random transforms so unit
-    tests have limited levels of flake.
-
-    Parameters
-    -------------
-    count : int
-      Number of repeatable but random-ish transforms to generate
-    translate : float
-      The scale of translation to apply.
-
-    Yields
-    ------------
-    transform : (4, 4) float
-      Homogenous transformation matrix
-    """
-    quaternion = random((count, 3))
-    translate = (random((count, 3)) - 0.5) * float(translate)
-
-    for quat, trans in zip(quaternion, translate):
-        matrix = tf.random_rotation_matrix(rand=quat)
-        matrix[:3, 3] = trans
-        yield matrix
-
-
-# random should be deterministic
-assert np.allclose(random(10), random(10))
-assert np.allclose(list(random_transforms(10)),
-                   list(random_transforms(10)))
-
-
 def _load_data():
     """
     Load the JSON files from our truth directory.
diff --git a/tests/helpers/id_helper.py b/tests/helpers/id_helper.py
index d0116863..fb5f09a9 100644
--- a/tests/helpers/id_helper.py
+++ b/tests/helpers/id_helper.py
@@ -72,9 +72,8 @@ def permutations(mesh,
         identifiers.append(identifier)
 
         if (time.time() - start) > cutoff:
-            print('bailing for time:{} count:{}'.format(
-                time.time() - start,
-                i))
+            print('bailing for time:{} count:{}'.format(time.time() - start,
+                                                        i))
             return np.array(identifiers)
 
     return np.array(identifiers)
diff --git a/tests/test_align.py b/tests/test_align.py
index 8fa42fb8..b8c266b2 100644
--- a/tests/test_align.py
+++ b/tests/test_align.py
@@ -20,8 +20,8 @@ class AlignTests(g.unittest.TestCase):
         # start with some edge cases and make sure the transform works
         target = g.np.array([0, 0, -1], dtype=g.np.float64)
         vectors = g.np.vstack((
-            g.trimesh.unitize(g.random((1000, 3)) - .5),
-            g.random((1000, 3)) - .5,
+            g.trimesh.unitize(g.np.random.random((1000, 3)) - .5),
+            g.np.random.random((1000, 3)) - .5,
             [-target, target],
             g.trimesh.util.generate_basis(target),
             [[7.12106798e-07, -7.43194705e-08, 1.00000000e+00],
diff --git a/tests/test_arc.py b/tests/test_arc.py
index 5aba967b..9c3df525 100644
--- a/tests/test_arc.py
+++ b/tests/test_arc.py
@@ -34,11 +34,11 @@ class ArcTests(g.unittest.TestCase):
         min_angle = g.np.radians(2)
         count = 1000
 
-        center_3D = (g.random((count, 3)) - .5) * 50
+        center_3D = (g.np.random.random((count, 3)) - .5) * 50
         center_2D = center_3D[:, 0:2]
-        radii = g.np.clip(g.random(count) * 100, min_angle, g.np.inf)
+        radii = g.np.clip(g.np.random.random(count) * 100, min_angle, g.np.inf)
 
-        angles = g.random((count, 2)) * \
+        angles = g.np.random.random((count, 2)) * \
             (g.np.pi - min_angle) + min_angle
         angles = g.np.column_stack((g.np.zeros(count),
                                     g.np.cumsum(angles, axis=1)))
diff --git a/tests/test_base.py b/tests/test_base.py
index 722daaee..5a6a52e3 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -35,7 +35,7 @@ class MeshTests(g.unittest.TestCase):
         assert g.np.isclose(m.volume, pre_vol)
 
         # add some unreferenced vertices
-        m.vertices = g.np.vstack((m.vertices, g.random((100, 3))))
+        m.vertices = g.np.vstack((m.vertices, g.np.random.random((100, 3))))
         assert len(m.vertices) == pre_len + 100
         assert g.np.isclose(m.volume, pre_vol)
 
diff --git a/tests/test_bounds.py b/tests/test_bounds.py
index a8340485..9208bfe2 100644
--- a/tests/test_bounds.py
+++ b/tests/test_bounds.py
@@ -69,7 +69,7 @@ class BoundsTest(g.unittest.TestCase):
         """
         for dimension in [3, 2]:
             for i in range(25):
-                points = g.random((10, dimension))
+                points = g.np.random.random((10, dimension))
                 to_origin, extents = g.trimesh.bounds.oriented_bounds(points)
 
                 assert g.trimesh.util.is_shape(to_origin,
@@ -93,7 +93,7 @@ class BoundsTest(g.unittest.TestCase):
     def test_2D(self):
         for theta in g.np.linspace(0, g.np.pi * 2, 2000):
             # create some random rectangular-ish 2D points
-            points = g.random((10, 2)) * [5, 1]
+            points = g.np.random.random((10, 2)) * [5, 1]
 
             # save the basic AABB of the points before rotation
             rectangle_pre = points.ptp(axis=0)
@@ -207,7 +207,7 @@ class BoundsTest(g.unittest.TestCase):
             # transform box randomly in rotation and translation
             mat = g.trimesh.transformations.random_rotation_matrix()
             # translate in box -100 : +100
-            mat[:3, 3] = (g.random(3) - .5) * 200
+            mat[:3, 3] = (g.np.random.random(3) - .5) * 200
 
             # source mesh to check
             b = g.trimesh.creation.box(extents=extents,
diff --git a/tests/test_cache.py b/tests/test_cache.py
index 4119f80f..2d402f36 100644
--- a/tests/test_cache.py
+++ b/tests/test_cache.py
@@ -26,7 +26,7 @@ class CacheTest(g.unittest.TestCase):
 
             # generate test data and perform numpy operations
             a = g.trimesh.caching.tracked_array(
-                g.random(TEST_DIM))
+                g.np.random.random(TEST_DIM))
             modified = [hash(a)]
             a[0][0] = 10
             modified.append(hash(a))
@@ -95,7 +95,7 @@ class CacheTest(g.unittest.TestCase):
         g.trimesh.caching.hash_fast = original
 
     def test_contiguous(self):
-        a = g.random((100, 3))
+        a = g.np.random.random((100, 3))
         t = g.trimesh.caching.tracked_array(a)
 
         original = g.trimesh.caching.hash_fast
@@ -120,7 +120,7 @@ class CacheTest(g.unittest.TestCase):
         """
         d = g.trimesh.caching.DataStore()
 
-        d['hi'] = g.random(100)
+        d['hi'] = g.np.random.random(100)
         hash_initial = hash(d)
         # mutate internal data
         d['hi'][0] += 1
diff --git a/tests/test_color.py b/tests/test_color.py
index 51027148..de1dc33a 100644
--- a/tests/test_color.py
+++ b/tests/test_color.py
@@ -93,30 +93,31 @@ class VisualTest(g.unittest.TestCase):
         m.visual.vertex_colors[1] = test_color_transparent
         assert m.visual.transparency
 
-        test = (g.random((len(m.faces), 4)) * 255).astype(g.np.uint8)
+        test = (g.np.random.random((len(m.faces), 4)) * 255).astype(g.np.uint8)
         m.visual.face_colors = test
         assert bool((m.visual.face_colors == test).all())
         assert m.visual.kind == 'face'
 
-        test = (g.random((len(m.vertices), 4))
+        test = (g.np.random.random((len(m.vertices), 4))
                 * 255).astype(g.np.uint8)
         m.visual.vertex_colors = test
         assert bool((m.visual.vertex_colors == test).all())
         assert m.visual.kind == 'vertex'
 
-        test = (g.random(4) * 255).astype(g.np.uint8)
+        test = (g.np.random.random(4) * 255).astype(g.np.uint8)
         m.visual.face_colors = test
         assert bool((m.visual.vertex_colors == test).all())
         assert m.visual.kind == 'face'
-        m.visual.vertex_colors[0] = [0, 0, 0, 0]
+        m.visual.vertex_colors[0] = (
+            g.np.random.random(4) * 255).astype(g.np.uint8)
         assert m.visual.kind == 'vertex'
 
-        test = (g.random(4) * 255).astype(g.np.uint8)
+        test = (g.np.random.random(4) * 255).astype(g.np.uint8)
         m.visual.vertex_colors = test
         assert bool((m.visual.face_colors == test).all())
         assert m.visual.kind == 'vertex'
-        m.visual.face_colors[:2] = (
-            g.random((2, 4)) * 255).astype(g.np.uint8)
+        m.visual.face_colors[0] = (
+            g.np.random.random(4) * 255).astype(g.np.uint8)
         assert m.visual.kind == 'face'
 
     def test_smooth(self):
diff --git a/tests/test_export.py b/tests/test_export.py
index 386e2a6d..48ab5a35 100644
--- a/tests/test_export.py
+++ b/tests/test_export.py
@@ -264,7 +264,7 @@ class ExportTest(g.unittest.TestCase):
         RET_COUNT = 5
 
         # a path that doesn't exist
-        nonexists = '/banana{}'.format(g.random())
+        nonexists = '/banana{}'.format(g.np.random.random())
         assert not g.os.path.exists(nonexists)
 
         # loadable OBJ model
diff --git a/tests/test_geom.py b/tests/test_geom.py
index a82d291e..0e4c86d7 100644
--- a/tests/test_geom.py
+++ b/tests/test_geom.py
@@ -12,8 +12,8 @@ class GeomTests(g.unittest.TestCase):
     def test_triangulate(self):
         from trimesh.geometry import triangulate_quads as tq
         # create some triangles and quads
-        tri = (g.random((100, 3)) * 100).astype(g.np.int64)
-        quad = (g.random((100, 4)) * 100).astype(g.np.int64)
+        tri = (g.np.random.random((100, 3)) * 100).astype(g.np.int64)
+        quad = (g.np.random.random((100, 4)) * 100).astype(g.np.int64)
 
         # should just exit early for triangles
         assert g.np.allclose(tri, tq(tri.tolist()))
diff --git a/tests/test_gltf.py b/tests/test_gltf.py
index 2bd6a220..e018e59f 100644
--- a/tests/test_gltf.py
+++ b/tests/test_gltf.py
@@ -695,7 +695,7 @@ class GLTFTest(g.unittest.TestCase):
         # test concatenation with texture
         m = g.get_mesh('fuze.obj')
 
-        colors = (g.random(
+        colors = (g.np.random.random(
             (len(m.vertices), 4)) * 255).astype(g.np.uint8)
 
         # set the color vertex attribute
diff --git a/tests/test_grouping.py b/tests/test_grouping.py
index c23eddcc..2fbb0236 100644
--- a/tests/test_grouping.py
+++ b/tests/test_grouping.py
@@ -195,7 +195,7 @@ class GroupTests(g.unittest.TestCase):
     def test_cluster(self):
         # create some random points stacked with some zeros to cluster
         points = g.np.vstack(((
-            g.random((10000, 3)) * 5).astype(g.np.int64),
+            g.np.random.random((10000, 3)) * 5).astype(g.np.int64),
             g.np.zeros((100, 3))))
         # should be at least one cluster
         assert len(g.trimesh.grouping.clusters(points, .01)) > 0
@@ -304,7 +304,7 @@ class GroupTests(g.unittest.TestCase):
 
     def test_unique_ordered_rows(self):
         # check the ordering of unique_rows
-        v = g.random((100000, 3))
+        v = g.np.random.random((100000, 3))
         v = g.np.vstack((v, v, v, v))
 
         # index, inverse
diff --git a/tests/test_identifier.py b/tests/test_identifier.py
index 60706af5..3df80a4a 100644
--- a/tests/test_identifier.py
+++ b/tests/test_identifier.py
@@ -7,6 +7,7 @@ except BaseException:
 class IdentifierTest(g.unittest.TestCase):
 
     def test_identifier(self, count=25):
+
         meshes = g.np.append(list(g.get_meshes(
             only_watertight=True, split=True, min_volume=0.001)),
             g.get_mesh('fixed_top.ply'))
@@ -19,8 +20,8 @@ class IdentifierTest(g.unittest.TestCase):
             g.log.info('Trying hash at %d random transforms', count)
             hashed = []
             identifier = []
-            for transform in g.random_transforms(count):
-                permutated = mesh.copy().apply_transform(transform)
+            for _ in range(count):
+                permutated = mesh.permutate.transform()
                 hashed.append(permutated.identifier_hash)
                 identifier.append(permutated.identifier)
 
@@ -36,13 +37,10 @@ class IdentifierTest(g.unittest.TestCase):
                             str(g.np.array(debug, dtype=g.np.int64)))
                 raise ValueError('values differ after transform!')
 
-            # stretch the mesh by a small amount
-            stretched = mesh.copy().apply_scale(
-                [0.99974507, 0.9995662, 1.0029832])
-            if hashed[-1] == stretched.identifier_hash:
-                raise ValueError(
-                    'Hashes on %s didn\'t change after stretching',
-                    mesh.metadata['file_name'])
+            if hashed[-1] == permutated.permutate.noise(
+                    mesh.scale / 100.0).identifier_hash:
+                raise ValueError('Hashes on %s didn\'t change after noise!',
+                                 mesh.metadata['file_name'])
 
     def test_scene_id(self):
         """
diff --git a/tests/test_inertia.py b/tests/test_inertia.py
index 601b4eaa..64f43b8d 100644
--- a/tests/test_inertia.py
+++ b/tests/test_inertia.py
@@ -112,7 +112,7 @@ class InertiaTest(g.unittest.TestCase):
                     matrix = g.trimesh.transformations.random_rotation_matrix()
                     p.primitive.transform = matrix
                 elif hasattr(p.primitive, 'center'):
-                    p.primitive.center = g.random(3)
+                    p.primitive.center = g.np.random.random(3)
 
     def test_tetrahedron(self):
         # Based on the 'numerical example' of the paper:
diff --git a/tests/test_integralmeancurvature.py b/tests/test_integralmeancurvature.py
index 58937fff..4f98f593 100644
--- a/tests/test_integralmeancurvature.py
+++ b/tests/test_integralmeancurvature.py
@@ -32,7 +32,7 @@ class IntegralMeanCurvatureTest(g.unittest.TestCase):
         # how close do we need to be - relative tolerance
         tol = 1e-3
         n_tests = 4
-        extents = 1 - g.random((n_tests, 3))
+        extents = 1 - g.np.random.random((n_tests, 3))
         for extent in extents:
             m = g.trimesh.creation.box(extents=extent)
             IMC = m.integral_mean_curvature
diff --git a/tests/test_merge.py b/tests/test_merge.py
index 4166bc17..4d361d3d 100644
--- a/tests/test_merge.py
+++ b/tests/test_merge.py
@@ -19,7 +19,7 @@ class MargeTest(g.unittest.TestCase):
 
         # stack a bunch of unreferenced vertices
         m.vertices = g.np.vstack((
-            m.vertices, g.random((10000, 3))))
+            m.vertices, g.np.random.random((10000, 3))))
         assert m.euler_number == 2
         assert m.vertices.shape == (10008, 3)
         assert m.referenced_vertices.sum() == 8
diff --git a/tests/test_mutate.py b/tests/test_mutate.py
index 71d3ff8d..747515b0 100644
--- a/tests/test_mutate.py
+++ b/tests/test_mutate.py
@@ -143,21 +143,21 @@ class MutateTests(g.unittest.TestCase):
 
         # ray.intersects_id
         centre = mesh.vertices.mean(axis=0)
-        origins = g.random((100, 3)) * 1000
+        origins = g.np.random.random((100, 3)) * 1000
         directions = g.np.copy(origins)
         directions[:50, :] -= centre
         directions[50:, :] += centre
         mesh.ray.intersects_id(origins, directions)
 
         # nearest.vertex
-        points = g.random((500, 3)) * 100
+        points = g.np.random.random((500, 3)) * 100
         mesh.nearest.vertex(points)
 
         # section
         section_count = 20
-        origins = g.random((section_count, 3)) * 100
-        normals = g.random((section_count, 3)) * 100
-        heights = g.random((10,)) * 100
+        origins = g.np.random.random((section_count, 3)) * 100
+        normals = g.np.random.random((section_count, 3)) * 100
+        heights = g.np.random.random((10,)) * 100
         for o, n in zip(origins, normals):
             # try slicing at random origin and at center mass
             mesh.slice_plane(o, n)
diff --git a/tests/test_nsphere.py b/tests/test_nsphere.py
index f773a862..c33d322c 100644
--- a/tests/test_nsphere.py
+++ b/tests/test_nsphere.py
@@ -27,7 +27,7 @@ class NSphereTest(g.unittest.TestCase):
         # check minimum n-sphere for points in 2, 3, 4 dimensions
         for d in [2, 3, 4]:
             for i in range(5):
-                points = g.random((100, d))
+                points = g.np.random.random((100, d))
                 C, R = g.trimesh.nsphere.minimum_nsphere(points)
                 R_check = ((points - C)**2).sum(axis=1).max() ** .5
                 assert len(C) == d
@@ -38,7 +38,7 @@ class NSphereTest(g.unittest.TestCase):
         # make sure created spheres are uv sphere
         m = g.trimesh.creation.uv_sphere()
         # move the mesh around for funsies
-        m.apply_translation(g.random(3))
+        m.apply_translation(g.np.random.random(3))
         m.apply_transform(
             g.trimesh.transformations.random_rotation_matrix())
         # all vertices should be on nsphere
diff --git a/tests/test_packing.py b/tests/test_packing.py
index ecf7315c..0f698df7 100644
--- a/tests/test_packing.py
+++ b/tests/test_packing.py
@@ -107,7 +107,7 @@ class PackingTest(g.unittest.TestCase):
         paths = [g.trimesh.load_path(i) for i in polygons]
 
         with g.Profiler() as P:
-            r, tf, consume = packing.paths(paths, spacing=0.02)
+            r, tf, consume = packing.paths(paths)
         print(P.output_text())
         # number of paths inserted
         count = consume.sum()
@@ -179,8 +179,8 @@ class PackingTest(g.unittest.TestCase):
                 extents = []
                 for i in ori:
                     extents.append(
-                        g.np.roll(i, int(g.random() * 10)) +
-                        g.random(3))
+                        g.np.roll(i, int(g.np.random.random() * 10)) +
+                        g.np.random.random(3))
                 extents = g.np.array(extents)
 
                 bounds, consume = packing.rectangles(extents)
@@ -238,15 +238,13 @@ class PackingTest(g.unittest.TestCase):
                 density.append(viz.volume / viz.bounding_box.volume)
         print(P.output_text())
 
-    def test_meshes(self, count=20):
+    def test_meshes(self):
         from trimesh.path import packing
         # create some random rotation boxes
         meshes = [g.trimesh.creation.box(
-            extents=extents,
-            transform=transform)
-            for transform, extents in zip(
-            g.random_transforms(count),
-            (g.random((count, 3)) + 1) * 10)]
+            extents=g.np.random.random(3),
+            transform=g.tf.random_rotation_matrix())
+            for _ in range(20)]
         packed, transforms, consume = packing.meshes(
             meshes, spacing=0.01)
         scene = g.trimesh.Scene(packed)
diff --git a/tests/test_points.py b/tests/test_points.py
index cb2387bd..459ba984 100644
--- a/tests/test_points.py
+++ b/tests/test_points.py
@@ -12,7 +12,7 @@ class PointsTest(g.unittest.TestCase):
         """
         shape = (100, 3)
         # random points
-        points = g.random(shape)
+        points = g.np.random.random(shape)
         # make sure randomness never gives duplicates by offsetting
         points += g.np.arange(shape[0]).reshape((-1, 1))
 
@@ -35,7 +35,7 @@ class PointsTest(g.unittest.TestCase):
         assert hash(cloud) == initial_hash
 
         # set some random colors
-        cloud.colors = g.random((shape[0], 4))
+        cloud.colors = g.np.random.random((shape[0], 4))
         # remove the duplicates we created
         cloud.merge_vertices()
 
@@ -80,8 +80,8 @@ class PointsTest(g.unittest.TestCase):
         vertices and no faces for some unknowable reason
         """
 
-        v = g.random((1000, 3))
-        v[g.np.floor(g.random(90) * len(v)).astype(int)] = v[0]
+        v = g.np.random.random((1000, 3))
+        v[g.np.floor(g.np.random.random(90) * len(v)).astype(int)] = v[0]
 
         mesh = g.trimesh.Trimesh(v)
 
@@ -94,7 +94,7 @@ class PointsTest(g.unittest.TestCase):
             # create a random rotation
             matrix = g.trimesh.transformations.random_rotation_matrix()
             # create some random points in spacd
-            p = g.random((1000, 3))
+            p = g.np.random.random((1000, 3))
             # make them all lie on the XY plane so we know
             # the correct normal to check against
             p[:, 2] = 0
@@ -116,7 +116,7 @@ class PointsTest(g.unittest.TestCase):
             matrices = [g.trimesh.transformations.random_rotation_matrix()
                         for _ in range(nb_points_sets)]
             # create some random points in spacd
-            p = g.random((nb_points_sets, 1000, 3))
+            p = g.np.random.random((nb_points_sets, 1000, 3))
             # make them all lie on the XY plane so we know
             # the correct normal to check against
             p[..., 2] = 0
@@ -171,7 +171,7 @@ class PointsTest(g.unittest.TestCase):
         for dimension in [2, 3]:
             for count in [2, 10, 100]:
                 for i in range(10):
-                    points = g.random((count, dimension))
+                    points = g.np.random.random((count, dimension))
 
                     # find a path that visits every point quickly
                     idx, dist = g.trimesh.points.tsp(points, start=0)
@@ -225,7 +225,7 @@ class PointsTest(g.unittest.TestCase):
         # initial color CRC
         initial = hash(p.visual)
         # set to random colors
-        p.colors = g.random(
+        p.colors = g.np.random.random(
             (len(p.vertices), 4))
         # visual CRC should have changed
         assert hash(p.visual) != initial
@@ -255,8 +255,8 @@ class PointsTest(g.unittest.TestCase):
         assert mask.shape == (200,)
 
     def test_add_operator(self):
-        points_1 = g.random((10, 3))
-        points_2 = g.random((20, 3))
+        points_1 = g.np.random.random((10, 3))
+        points_2 = g.np.random.random((20, 3))
         colors_1 = [[123, 123, 123, 255]] * len(points_1)
         colors_2 = [[255, 0, 123, 255]] * len(points_2)
 
diff --git a/tests/test_polygons.py b/tests/test_polygons.py
index 285047c4..72472d2e 100644
--- a/tests/test_polygons.py
+++ b/tests/test_polygons.py
@@ -90,7 +90,7 @@ class PolygonTests(g.unittest.TestCase):
         m = g.trimesh.creation.icosphere(subdivisions=4)
 
         p = [g.trimesh.path.polygons.projected(m, normal=n)
-             for n in g.random((100, 3))]
+             for n in g.np.random.random((100, 3))]
 
         # sphere projection should never have interiors
         assert all(len(i.interiors) == 0 for i in p)
@@ -144,28 +144,6 @@ class PolygonTests(g.unittest.TestCase):
                 holes = []
             return Polygon(shell=shell, holes=holes)
 
-        def poly_corner(bh):
-            # return a rectangle with one corner
-            # at the origin and the rest in positive space
-            shell = rectangle(bh)
-            shell += shell.min(axis=0)
-            return Polygon(shell=shell)
-
-        def poly_doublecorner(bh):
-            # returns two equal sized rectangles as one polygon
-            # Same as poly_corner(), but the rectangle gets
-            # mirrored by 180 deg at the origin
-            # This puts the centroid in the origin with Ixy != 0
-            shell_1 = rectangle(bh)
-            shell_1 += shell_1.min(axis=0)
-            shell_2 = - shell_1
-            shell = g.np.concatenate((shell_1[2:, :],
-                                      shell_1[:2, :],
-                                      shell_2[2:, :],
-                                      shell_2[:2, :]
-                                      ), axis=0)
-            return Polygon(shell=shell)
-
         def truth(bh, bhi=None):
             # return the analytical second moment of area
             # for a rectangle with centroid at the origin
@@ -183,91 +161,46 @@ class PolygonTests(g.unittest.TestCase):
             # check a rectangle with one corner
             # at the origin and the rest in positive space
             b, h = bh
-            return g.np.array([b * h**3 / 3.0,
-                               h * b**3 / 3.0,
-                               0.5 * b**2 * 0.5 * h**2], dtype=g.np.float64)
-
-        from trimesh.path.polygons import second_moments
-        from trimesh.path.polygons import transform_polygon
+            return g.np.array([b * h**3,
+                               h * b**3], dtype=g.np.float64) / 3.0
 
+        from trimesh.path.polygons import second_moment
         from shapely.geometry import Polygon
-
         heights = g.np.array([[0.01, 0.01],
                               [1, 1],
                               [10, 2],
                               [3, 21]])
         for bh in heights:
             # check the second moment of a rectangle
-            # as polygon is already centered, centered doesn't have any effect
-            O_moments, O_principal_moments, O_alpha, O_transform = second_moments(
-                poly(bh), centered=True)
+            # as both a numpy array and as a Polygon
+            a = second_moment(rectangle(bh))
+            b = second_moment(poly(bh))
             # check against wikipedia
             t = truth(bh)
-            # for a centered rectangle, the principal axis are alread aligned
-            # with the frame axis
-            assert g.np.allclose(O_moments, t)
-            assert g.np.any(g.np.isclose(O_moments, O_principal_moments[0]))
-            assert g.np.isclose(O_moments[2], 0)  # Ixy = 0
-            assert g.np.isclose(O_alpha, 0)
-            assert g.np.allclose(O_transform, g.np.eye(3))
-
-            # now check a rectangle with the corner, so Ixy != 0
-
-            # First we test with centering. The results should be same as
-            # with the initally centered rectangles
-            C_moments, C_principal_moments, C_alpha, C_transform = second_moments(
-                poly_corner(bh), centered=True)
-            assert g.np.allclose(O_moments, C_moments)
-            assert g.np.allclose(O_principal_moments, C_principal_moments)
-            assert g.np.isclose(O_alpha, C_alpha)
-            assert g.np.allclose(O_transform[:, :2], C_transform[:, :2])
-
-            # Now without centering
-            moments = second_moments(poly_corner(bh), centered=False)
-            t = truth_corner(bh)
-            assert g.np.allclose(moments, t)
-
-            # Now we will get the transform for a double rectangle. Then we will apply
-            # the transform and test if Ixy == 0, alpha == 0 etc.
-            C_moments, C_principal_moments, C_alpha, C_transform = second_moments(
-                poly_doublecorner(bh), centered=True)
-            # apply the outputted transform to the polygon
-            T_polygon = transform_polygon(poly_doublecorner(bh), C_transform)
-            # call the function on the transformed polygon
-            T_moments, T_principal_moments, T_alpha, T_transform = second_moments(
-                T_polygon, centered=True)
-            assert g.np.any(g.np.isclose(T_moments, C_principal_moments[0]))
-            assert g.np.allclose(C_principal_moments, T_principal_moments)
-            assert g.np.isclose(T_alpha, 0, atol=1e-7)
-            assert g.np.allclose(T_transform, g.np.eye(3))
-
-            # check polygons with interior
+            assert g.np.allclose(a, b)
+            assert g.np.allclose(a, t)
+
+            # now add some interiors
             for bhi in heights:
                 # only check if interior is smaller than exterior
                 if not (bhi < bh).all():
                     continue
 
                 # check a rectangle with interiors
-                c = second_moments(poly(bh, bhi), centered=False)
+                c = second_moment(poly(bh, bhi))
                 t = truth(bh, bhi)
                 assert g.np.allclose(c, t)
 
-    def test_native_centroid(self):
-        # check our native implementation of the shoelace algorithm
-        # against the results from shapely which we use for native
-        # checks of counter-clockwise 2D coordinate loops
-
-        # get some polygons without interiors
-        polygons = [g.Polygon(i) for i in g.data['nestable']]
-        # same data as (n, 2) float arrays
-        coords = [g.np.array(i) for i in g.data['nestable']]
-
-        for p, c in zip(polygons, coords):
-            # area will be signed with respect to counter-clockwise
-            ccw, area, centroid = g.trimesh.util.is_ccw(c, return_all=True)
-            assert g.np.allclose(
-                centroid, g.np.array(p.centroid.coords)[0])
-            assert g.np.isclose(abs(area), p.area)
+            # now check a rectangle with the corner
+            # at the origin to see if we've done something
+            # silly with regards to offsets
+            r = rectangle(bh)
+            r += r.min(axis=0)
+            a = second_moment(r)
+            t = truth_corner(bh)
+            # not sure what Ixy is analytically because
+            # wikipedia didn't list it for this case
+            assert g.np.allclose(a[:2], t)
 
 
 if __name__ == '__main__':
diff --git a/tests/test_proximity.py b/tests/test_proximity.py
index 63be516a..d1015274 100644
--- a/tests/test_proximity.py
+++ b/tests/test_proximity.py
@@ -36,7 +36,7 @@ class NearestTest(g.unittest.TestCase):
     def test_helper(self):
         # just make sure the plumbing returns something
         for mesh in g.get_meshes(2):
-            points = (g.random((100, 3)) - .5) * 100
+            points = (g.np.random.random((100, 3)) - .5) * 100
 
             a = mesh.nearest.on_surface(points)
             assert a is not None
@@ -210,7 +210,7 @@ class NearestTest(g.unittest.TestCase):
 
     def test_candidates(self):
         mesh = g.trimesh.creation.random_soup(2000)
-        points = g.random((2000, 3))
+        points = g.np.random.random((2000, 3))
         g.trimesh.proximity.nearby_faces(
             mesh=mesh, points=points)
 
diff --git a/tests/test_ray.py b/tests/test_ray.py
index f3e6e77b..d94ea744 100644
--- a/tests/test_ray.py
+++ b/tests/test_ray.py
@@ -33,7 +33,7 @@ class RayTests(g.unittest.TestCase):
             sphere = g.get_mesh('unit_sphere.STL',
                                 use_embree=use_embree)
 
-            ray_origins = g.random(dimension)
+            ray_origins = g.np.random.random(dimension)
             ray_directions = g.np.tile([0, 0, 1], (dimension[0], 1))
             ray_origins[:, 2] = -5
 
@@ -66,7 +66,7 @@ class RayTests(g.unittest.TestCase):
             sphere = g.get_mesh('unit_sphere.STL',
                                 use_embree=use_embree)
             # should never hit the sphere
-            ray_origins = g.random(dimension)
+            ray_origins = g.np.random.random(dimension)
             ray_directions = g.np.tile([0, 1, 0], (dimension[0], 1))
             ray_origins[:, 2] = -5
 
@@ -94,7 +94,7 @@ class RayTests(g.unittest.TestCase):
             assert not test_out.any()
 
             points_way_out = (
-                g.random(
+                g.np.random.random(
                     (30, 3)) * 100) + 1.0 + mesh.bounds[1]
             test_way_out = mesh.ray.contains_points(points_way_out)
             assert not test_way_out.any()
diff --git a/tests/test_registration.py b/tests/test_registration.py
index 10267ac0..4519c80d 100644
--- a/tests/test_registration.py
+++ b/tests/test_registration.py
@@ -13,11 +13,11 @@ class RegistrationTest(g.unittest.TestCase):
         opt = g.itertools.combinations([True, False] * 6, 6)
         for reflection, translation, scale, a_flip, a_scale, weight in opt:
             # create random points in space
-            points_a = (g.random((1000, 3)) - .5) * 1000
+            points_a = (g.np.random.random((1000, 3)) - .5) * 1000
             # create a random transform
             matrix = g.trimesh.transformations.random_rotation_matrix()
             # add a translation component to transform
-            matrix[:3, 3] = g.random(3) * 100
+            matrix[:3, 3] = g.np.random.random(3) * 100
             # apply a flip (reflection) to test data
             if a_flip:
                 matrix = g.np.dot(
@@ -34,7 +34,7 @@ class RegistrationTest(g.unittest.TestCase):
 
             # weight points or not
             if weight:
-                weights = (g.random(len(points_a)) + 9) / 10
+                weights = (g.np.random.random(len(points_a)) + 9) / 10
             else:
                 weights = None
 
@@ -114,7 +114,7 @@ class RegistrationTest(g.unittest.TestCase):
     def test_icp_points(self):
         # see if ICP alignment works with point clouds
         # create random points in space
-        points_a = (g.random((1000, 3)) - .5) * 1000
+        points_a = (g.np.random.random((1000, 3)) - .5) * 1000
         # create a random transform
         # matrix = g.trimesh.transformations.random_rotation_matrix()
         # create a small transform
@@ -150,7 +150,7 @@ class RegistrationTest(g.unittest.TestCase):
         mesh = mesh.permutate.noise(noise)
         # randomly rotation with translation
         transform = g.trimesh.transformations.random_rotation_matrix()
-        transform[:3, 3] = (g.random(3) - .5) * 1000
+        transform[:3, 3] = (g.np.random.random(3) - .5) * 1000
 
         mesh.apply_transform(transform)
 
diff --git a/tests/test_render.py b/tests/test_render.py
index 8cbac6db..2a56f752 100644
--- a/tests/test_render.py
+++ b/tests/test_render.py
@@ -50,13 +50,13 @@ class RenderTest(g.unittest.TestCase):
         assert len(args) == 6
         assert len(args_auto) == len(args)
 
-        P22 = g.random((100, 2))
+        P22 = g.np.random.random((100, 2))
         args = rendering.points_to_vertexlist(P22)
         args_auto = rendering.convert_to_vertexlist(P22)
         assert len(args) == 6
         assert len(args_auto) == len(args)
 
-        P31 = g.random((100, 3))
+        P31 = g.np.random.random((100, 3))
         args = rendering.points_to_vertexlist(P31)
         args_auto = rendering.convert_to_vertexlist(P31)
         assert len(args) == 6
diff --git a/tests/test_resolvers.py b/tests/test_resolvers.py
index 3017e071..42a51e29 100644
--- a/tests/test_resolvers.py
+++ b/tests/test_resolvers.py
@@ -29,27 +29,16 @@ class ResolverTest(g.unittest.TestCase):
 
     def test_web_namespace(self):
         base = 'https://example.com'
-        name = 'stuff'
-        target = 'hi.gltf'
-
+        ns = 'stuff'
         # check with a trailing slash
         a = g.trimesh.resolvers.WebResolver(base + '/')
-        b = g.trimesh.resolvers.WebResolver(base + '//')
-        c = g.trimesh.resolvers.WebResolver(base)
-        d = a.namespaced(name)
-
-        # base URL's should always be the same with one trailing slash
-        assert a.base_url == b.base_url
-        assert b.base_url == c.base_url
-        assert c.base_url == base + '/'
-        # check namespaced
-        assert d.base_url == base + '/' + name + '/'
+        b = a.namespaced(ns)
 
         # should have correct slashes
-        truth = '/'.join([base, name, target])
-
-        assert a.base_url + name + '/' + target == truth
-        assert d.base_url + target == truth
+        truth = base + '/' + ns
+        assert b.base_url == truth
+        # should not have altered original
+        assert a.base_url == base + '/'
 
     def test_items(self):
         # check __getitem__ and __setitem__
diff --git a/tests/test_scenegraph.py b/tests/test_scenegraph.py
index 7c3fcc4f..c34d7cb7 100644
--- a/tests/test_scenegraph.py
+++ b/tests/test_scenegraph.py
@@ -7,7 +7,7 @@ from trimesh.scene.transforms import EnforcedForest
 
 
 def random_chr():
-    return chr(ord('a') + int(round(g.random() * 25)))
+    return chr(ord('a') + int(round(g.np.random.random() * 25)))
 
 
 class GraphTests(g.unittest.TestCase):
@@ -82,7 +82,7 @@ class GraphTests(g.unittest.TestCase):
         assert g.np.allclose(f(), g.np.eye(4))
 
         # a passed matrix should return immediately
-        fix = g.random((4, 4))
+        fix = g.np.random.random((4, 4))
         assert g.np.allclose(f(matrix=fix), fix)
 
         quat = g.trimesh.unitize([1, 2, 3, 1])
@@ -205,13 +205,13 @@ class GraphTests(g.unittest.TestCase):
             n=1000, seed=0, create_using=g.nx.DiGraph)
         for e in tree.edges:
             data = {}
-            if g.random() > .5:
+            if g.np.random.random() > .5:
                 # if a matrix is omitted but an edge exists it is
                 # the same as passing an identity matrix
                 data['matrix'] = tf.random_rotation_matrix()
-            if g.random() > .4:
+            if g.np.random.random() > .4:
                 # a geometry is not required for a node
-                data['geometry'] = str(int(g.random() * 1e8))
+                data['geometry'] = str(int(g.np.random.random() * 1e8))
             edgelist[e] = data
 
         # now apply the random data to an EnforcedForest
diff --git a/tests/test_section.py b/tests/test_section.py
index 752821ed..d05f90bc 100644
--- a/tests/test_section.py
+++ b/tests/test_section.py
@@ -136,11 +136,11 @@ class PlaneLine(g.unittest.TestCase):
         z = g.np.linspace(-1, 1, count)
 
         plane_origins = g.np.column_stack((
-            g.random((count, 2)), z))
+            g.np.random.random((count, 2)), z))
         plane_normals = g.np.tile([0, 0, -1], (count, 1))
 
         line_origins = g.np.tile([0, 0, 0], (count, 1))
-        line_directions = g.random((count, 3))
+        line_directions = g.np.random.random((count, 3))
 
         i, valid = g.trimesh.intersections.planes_lines(
             plane_origins=plane_origins,
diff --git a/tests/test_segments.py b/tests/test_segments.py
index 3a127559..b48fc56f 100644
--- a/tests/test_segments.py
+++ b/tests/test_segments.py
@@ -12,7 +12,7 @@ class SegmentsTest(g.unittest.TestCase):
         # check 2D and 3D
         for dimension in [2, 3]:
             # a bunch of random line segments
-            s = g.random((100, 2, dimension))
+            s = g.np.random.random((100, 2, dimension))
             # convert segment to point on line closest to origin
             # as well as a vector and two distances along vector
             param = segments.segments_to_parameters(s)
diff --git a/tests/test_stl.py b/tests/test_stl.py
index 1e9e1a81..eb3855e3 100644
--- a/tests/test_stl.py
+++ b/tests/test_stl.py
@@ -36,7 +36,7 @@ class STLTests(g.unittest.TestCase):
 
         len_vertices = len(m.vertices)
         # assign some random vertex attributes
-        random = g.random(len(m.vertices))
+        random = g.np.random.random(len(m.vertices))
         m.vertex_attributes['random'] = random
         m.vertex_attributes['nah'] = 20
 
diff --git a/tests/test_transformations.py b/tests/test_transformations.py
index a32fd9e2..42dd60f5 100644
--- a/tests/test_transformations.py
+++ b/tests/test_transformations.py
@@ -65,11 +65,11 @@ class TransformTest(g.unittest.TestCase):
 
     def test_around(self):
         # check transform_around on 2D points
-        points = g.random((100, 2))
+        points = g.np.random.random((100, 2))
         for i, p in enumerate(points):
-            offset = g.random(2)
+            offset = g.np.random.random(2)
             matrix = g.trimesh.transformations.planar_matrix(
-                theta=g.random() + .1,
+                theta=g.np.random.random() + .1,
                 offset=offset,
                 point=p)
 
@@ -82,7 +82,7 @@ class TransformTest(g.unittest.TestCase):
             assert compare.all(axis=1).sum() == 1
 
         # check transform_around on 3D points
-        points = g.random((100, 3))
+        points = g.np.random.random((100, 3))
         for i, p in enumerate(points):
             matrix = g.trimesh.transformations.random_rotation_matrix()
             matrix = g.trimesh.transformations.transform_around(matrix, p)
@@ -106,9 +106,9 @@ class TransformTest(g.unittest.TestCase):
                                       [0, 0, 0, 1]),
                              [1, -1, 0, 1])
 
-        angle = (g.random() - 0.5) * (2 * g.np.pi)
-        direc = g.random(3) - 0.5
-        point = g.random(3) - 0.5
+        angle = (g.np.random.random() - 0.5) * (2 * g.np.pi)
+        direc = g.np.random.random(3) - 0.5
+        point = g.np.random.random(3) - 0.5
         R0 = rotation_matrix(angle, direc, point)
         R1 = rotation_matrix(angle - 2 * g.np.pi, direc, point)
         assert g.trimesh.transformations.is_same_transform(R0, R1)
diff --git a/tests/test_util.py b/tests/test_util.py
index 75bd6020..6d720982 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -48,7 +48,7 @@ class UtilTests(unittest.TestCase):
     def test_bounds_tree(self):
         for attempt in range(3):
             for dimension in [2, 3]:
-                t = g.random((1000, 3, dimension))
+                t = g.np.random.random((1000, 3, dimension))
                 bounds = g.np.column_stack((t.min(axis=1), t.max(axis=1)))
                 tree = g.trimesh.util.bounds_tree(bounds)
                 self.assertTrue(0 in tree.intersection(bounds[0]))
@@ -57,7 +57,7 @@ class UtilTests(unittest.TestCase):
         # shortcut to the function
         f = g.trimesh.util.stack_3D
         # start with some random points
-        p = g.random((100, 2))
+        p = g.np.random.random((100, 2))
         stack = f(p)
         # shape should be 3D
         assert stack.shape == (100, 3)
@@ -205,36 +205,6 @@ class UtilTests(unittest.TestCase):
             names.add(unique_name('', names))
         assert len(names) == count
 
-        # Try with a larger set of names
-        # get some random strings
-        names = [g.uuid4().hex for _ in range(20)]
-        # make it a whole lotta duplicates
-        names = names * 1000
-        # add a non-int postfix to test
-        names.extend(['suppp_hi'] * 10)
-
-        assigned = set()
-        with g.Profiler() as P:
-            for name in names:
-                assigned.add(unique_name(name, assigned))
-        print(P.output_text())
-
-        assigned_new = set()
-        # tracker = UniqueName()\
-        counts = {}
-        with g.Profiler() as P:
-            for name in names:
-                assigned_new.add(unique_name(
-                    name,
-                    contains=assigned_new,
-                    counts=counts))
-        print(P.output_text())
-
-        # new scheme should match the old one
-        assert assigned_new == assigned
-        # de-duplicated set should match original length
-        assert len(assigned) == len(names)
-
 
 class ContainsTest(unittest.TestCase):
 
@@ -264,7 +234,7 @@ class IOWrapTests(unittest.TestCase):
         util = g.trimesh.util
 
         # check wrap_as_stream
-        test_b = g.random(1).tobytes()
+        test_b = g.np.random.random(1).tobytes()
         test_s = 'this is a test yo'
         res_b = util.wrap_as_stream(test_b).read()
         res_s = util.wrap_as_stream(test_s).read()
diff --git a/tests/test_vector.py b/tests/test_vector.py
index 61cdb1f4..ff356adc 100644
--- a/tests/test_vector.py
+++ b/tests/test_vector.py
@@ -11,7 +11,7 @@ class SphericalTests(g.unittest.TestCase):
         Convert vectors to spherical coordinates
         """
         # random unit vectors
-        v = g.trimesh.unitize(g.random((1000, 3)) - .5)
+        v = g.trimesh.unitize(g.np.random.random((1000, 3)) - .5)
         # (n, 2) angles in radians
         spherical = g.trimesh.util.vector_to_spherical(v)
         # back to unit vectors
@@ -26,7 +26,7 @@ class HemisphereTests(g.unittest.TestCase):
         for dimension in [2, 3]:
             # random unit vectors
             v = g.trimesh.unitize(
-                g.random((10000, dimension)) - .5)
+                g.np.random.random((10000, dimension)) - .5)
 
             # add some on- axis points
             v[:dimension] = g.np.eye(dimension)