Coverage for /Volumes/workspace/numpy-stl/stl/base.py: 41%

334 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-12-14 03:29 +0100

1import enum 

2import math 

3import numpy 

4import logging 

5try: # pragma: no cover 

6 from collections import abc 

7except ImportError: # pragma: no cover 

8 import collections as abc 

9 

10from python_utils import logger 

11 

12 

13#: When removing empty areas, remove areas that are smaller than this 

14AREA_SIZE_THRESHOLD = 0 

15#: Vectors in a point 

16VECTORS = 3 

17#: Dimensions used in a vector 

18DIMENSIONS = 3 

19 

20 

21class Dimension(enum.IntEnum): 

22 #: X index (for example, `mesh.v0[0][X]`) 

23 X = 0 

24 #: Y index (for example, `mesh.v0[0][Y]`) 

25 Y = 1 

26 #: Z index (for example, `mesh.v0[0][Z]`) 

27 Z = 2 

28 

29 

30# For backwards compatibility, leave the original references 

31X = Dimension.X 

32Y = Dimension.Y 

33Z = Dimension.Z 

34 

35 

36class RemoveDuplicates(enum.Enum): 

37 ''' 

38 Choose whether to remove no duplicates, leave only a single of the 

39 duplicates or remove all duplicates (leaving holes). 

40 ''' 

41 NONE = 0 

42 SINGLE = 1 

43 ALL = 2 

44 

45 @classmethod 

46 def map(cls, value): 

47 if value is True: 

48 value = cls.SINGLE 

49 elif value and value in cls: 

50 pass 

51 else: 

52 value = cls.NONE 

53 

54 return value 

55 

56 

57def logged(class_): 

58 # For some reason the Logged baseclass is not properly initiated on Linux 

59 # systems while this works on OS X. Please let me know if you can tell me 

60 # what silly mistake I made here 

61 

62 logger_name = logger.Logged._Logged__get_name( 

63 __name__, 

64 class_.__name__, 

65 ) 

66 

67 class_.logger = logging.getLogger(logger_name) 

68 

69 for key in dir(logger.Logged): 

70 if not key.startswith('__'): 

71 setattr(class_, key, getattr(class_, key)) 

72 

73 return class_ 

74 

75 

76@logged 

77class BaseMesh(logger.Logged, abc.Mapping): 

78 ''' 

79 Mesh object with easy access to the vectors through v0, v1 and v2. 

80 The normals, areas, min, max and units are calculated automatically. 

81 

82 :param numpy.array data: The data for this mesh 

83 :param bool calculate_normals: Whether to calculate the normals 

84 :param bool remove_empty_areas: Whether to remove triangles with 0 area 

85 (due to rounding errors for example) 

86 

87 :ivar str name: Name of the solid, only exists in ASCII files 

88 :ivar numpy.array data: Data as :func:`BaseMesh.dtype` 

89 :ivar numpy.array points: All points (Nx9) 

90 :ivar numpy.array normals: Normals for this mesh, calculated automatically 

91 by default (Nx3) 

92 :ivar numpy.array vectors: Vectors in the mesh (Nx3x3) 

93 :ivar numpy.array attr: Attributes per vector (used by binary STL) 

94 :ivar numpy.array x: Points on the X axis by vertex (Nx3) 

95 :ivar numpy.array y: Points on the Y axis by vertex (Nx3) 

96 :ivar numpy.array z: Points on the Z axis by vertex (Nx3) 

97 :ivar numpy.array v0: Points in vector 0 (Nx3) 

98 :ivar numpy.array v1: Points in vector 1 (Nx3) 

99 :ivar numpy.array v2: Points in vector 2 (Nx3) 

100 

101 >>> data = numpy.zeros(10, dtype=BaseMesh.dtype) 

102 >>> mesh = BaseMesh(data, remove_empty_areas=False) 

103 >>> # Increment vector 0 item 0 

104 >>> mesh.v0[0] += 1 

105 >>> mesh.v1[0] += 2 

106 

107 >>> # Check item 0 (contains v0, v1 and v2) 

108 >>> assert numpy.array_equal( 

109 ... mesh[0], 

110 ... numpy.array([1., 1., 1., 2., 2., 2., 0., 0., 0.])) 

111 >>> assert numpy.array_equal( 

112 ... mesh.vectors[0], 

113 ... numpy.array([[1., 1., 1.], 

114 ... [2., 2., 2.], 

115 ... [0., 0., 0.]])) 

116 >>> assert numpy.array_equal( 

117 ... mesh.v0[0], 

118 ... numpy.array([1., 1., 1.])) 

119 >>> assert numpy.array_equal( 

120 ... mesh.points[0], 

121 ... numpy.array([1., 1., 1., 2., 2., 2., 0., 0., 0.])) 

122 >>> assert numpy.array_equal( 

123 ... mesh.data[0], 

124 ... numpy.array(( 

125 ... [0., 0., 0.], 

126 ... [[1., 1., 1.], [2., 2., 2.], [0., 0., 0.]], 

127 ... [0]), 

128 ... dtype=BaseMesh.dtype)) 

129 >>> assert numpy.array_equal(mesh.x[0], numpy.array([1., 2., 0.])) 

130 

131 >>> mesh[0] = 3 

132 >>> assert numpy.array_equal( 

133 ... mesh[0], 

134 ... numpy.array([3., 3., 3., 3., 3., 3., 3., 3., 3.])) 

135 

136 >>> len(mesh) == len(list(mesh)) 

137 True 

138 >>> (mesh.min_ < mesh.max_).all() 

139 True 

140 >>> mesh.update_normals() 

141 >>> mesh.units.sum() 

142 0.0 

143 >>> mesh.v0[:] = mesh.v1[:] = mesh.v2[:] = 0 

144 >>> mesh.points.sum() 

145 0.0 

146 

147 >>> mesh.v0 = mesh.v1 = mesh.v2 = 0 

148 >>> mesh.x = mesh.y = mesh.z = 0 

149 

150 >>> mesh.attr = 1 

151 >>> (mesh.attr == 1).all() 

152 True 

153 

154 >>> mesh.normals = 2 

155 >>> (mesh.normals == 2).all() 

156 True 

157 

158 >>> mesh.vectors = 3 

159 >>> (mesh.vectors == 3).all() 

160 True 

161 

162 >>> mesh.points = 4 

163 >>> (mesh.points == 4).all() 

164 True 

165 ''' 

166 #: - normals: :func:`numpy.float32`, `(3, )` 

167 #: - vectors: :func:`numpy.float32`, `(3, 3)` 

168 #: - attr: :func:`numpy.uint16`, `(1, )` 

169 dtype = numpy.dtype([ 

170 ('normals', numpy.float32, (3, )), 

171 ('vectors', numpy.float32, (3, 3)), 

172 ('attr', numpy.uint16, (1, )), 

173 ]) 

174 dtype = dtype.newbyteorder('<') # Even on big endian arches, use little e. 

175 

176 def __init__(self, data, calculate_normals=True, 

177 remove_empty_areas=False, 

178 remove_duplicate_polygons=RemoveDuplicates.NONE, 

179 name='', speedups=True, **kwargs): 

180 super(BaseMesh, self).__init__(**kwargs) 

181 self.speedups = speedups 

182 if remove_empty_areas: 

183 data = self.remove_empty_areas(data) 

184 

185 if RemoveDuplicates.map(remove_duplicate_polygons).value: 

186 data = self.remove_duplicate_polygons(data, 

187 remove_duplicate_polygons) 

188 

189 self.name = name 

190 self.data = data 

191 

192 if calculate_normals: 

193 self.update_normals() 

194 

195 @property 

196 def attr(self): 

197 return self.data['attr'] 

198 

199 @attr.setter 

200 def attr(self, value): 

201 self.data['attr'] = value 

202 

203 @property 

204 def normals(self): 

205 return self.data['normals'] 

206 

207 @normals.setter 

208 def normals(self, value): 

209 self.data['normals'] = value 

210 

211 @property 

212 def vectors(self): 

213 return self.data['vectors'] 

214 

215 @vectors.setter 

216 def vectors(self, value): 

217 self.data['vectors'] = value 

218 

219 @property 

220 def points(self): 

221 return self.vectors.reshape(self.data.size, 9) 

222 

223 @points.setter 

224 def points(self, value): 

225 self.points[:] = value 

226 

227 @property 

228 def v0(self): 

229 return self.vectors[:, 0] 

230 

231 @v0.setter 

232 def v0(self, value): 

233 self.vectors[:, 0] = value 

234 

235 @property 

236 def v1(self): 

237 return self.vectors[:, 1] 

238 

239 @v1.setter 

240 def v1(self, value): 

241 self.vectors[:, 1] = value 

242 

243 @property 

244 def v2(self): 

245 return self.vectors[:, 2] 

246 

247 @v2.setter 

248 def v2(self, value): 

249 self.vectors[:, 2] = value 

250 

251 @property 

252 def x(self): 

253 return self.points[:, Dimension.X::3] 

254 

255 @x.setter 

256 def x(self, value): 

257 self.points[:, Dimension.X::3] = value 

258 

259 @property 

260 def y(self): 

261 return self.points[:, Dimension.Y::3] 

262 

263 @y.setter 

264 def y(self, value): 

265 self.points[:, Dimension.Y::3] = value 

266 

267 @property 

268 def z(self): 

269 return self.points[:, Dimension.Z::3] 

270 

271 @z.setter 

272 def z(self, value): 

273 self.points[:, Dimension.Z::3] = value 

274 

275 @classmethod 

276 def remove_duplicate_polygons(cls, data, value=RemoveDuplicates.SINGLE): 

277 value = RemoveDuplicates.map(value) 

278 polygons = data['vectors'].sum(axis=1) 

279 # Get a sorted list of indices 

280 idx = numpy.lexsort(polygons.T) 

281 # Get the indices of all different indices 

282 diff = numpy.any(polygons[idx[1:]] != polygons[idx[:-1]], axis=1) 

283 

284 if value is RemoveDuplicates.SINGLE: 

285 # Only return the unique data, the True is so we always get at 

286 # least the originals 

287 return data[numpy.sort(idx[numpy.concatenate(([True], diff))])] 

288 elif value is RemoveDuplicates.ALL: 

289 # We need to return both items of the shifted diff 

290 diff_a = numpy.concatenate(([True], diff)) 

291 diff_b = numpy.concatenate((diff, [True])) 

292 diff = numpy.concatenate((diff, [False])) 

293 

294 # Combine both unique lists 

295 filtered_data = data[numpy.sort(idx[diff_a & diff_b])] 

296 if len(filtered_data) <= len(data) / 2: 

297 return data[numpy.sort(idx[diff_a])] 

298 else: 

299 return data[numpy.sort(idx[diff])] 

300 else: 

301 return data 

302 

303 @classmethod 

304 def remove_empty_areas(cls, data): 

305 vectors = data['vectors'] 

306 v0 = vectors[:, 0] 

307 v1 = vectors[:, 1] 

308 v2 = vectors[:, 2] 

309 normals = numpy.cross(v1 - v0, v2 - v0) 

310 squared_areas = (normals ** 2).sum(axis=1) 

311 return data[squared_areas > AREA_SIZE_THRESHOLD ** 2] 

312 

313 def update_normals(self, update_areas=True, update_centroids=True): 

314 '''Update the normals, areas, and centroids for all points''' 

315 normals = numpy.cross(self.v1 - self.v0, self.v2 - self.v0) 

316 

317 if update_areas: 

318 self.update_areas(normals) 

319 

320 if update_centroids: 

321 self.update_centroids() 

322 

323 self.normals[:] = normals 

324 

325 def get_unit_normals(self): 

326 normals = self.normals.copy() 

327 normal = numpy.linalg.norm(normals, axis=1) 

328 non_zero = normal > 0 

329 if non_zero.any(): 

330 normals[non_zero] /= normal[non_zero][:, None] 

331 return normals 

332 

333 def update_min(self): 

334 self._min = self.vectors.min(axis=(0, 1)) 

335 

336 def update_max(self): 

337 self._max = self.vectors.max(axis=(0, 1)) 

338 

339 def update_areas(self, normals=None): 

340 if normals is None: 

341 normals = numpy.cross(self.v1 - self.v0, self.v2 - self.v0) 

342 

343 areas = .5 * numpy.sqrt((normals ** 2).sum(axis=1)) 

344 self.areas = areas.reshape((areas.size, 1)) 

345 

346 def update_centroids(self): 

347 self.centroids = numpy.mean([self.v0, self.v1, self.v2], axis=0) 

348 

349 def check(self): 

350 '''Check the mesh is valid or not''' 

351 return self.is_closed() 

352 

353 def is_closed(self): # pragma: no cover 

354 """Check the mesh is closed or not""" 

355 if numpy.isclose(self.normals.sum(axis=0), 0, atol=1e-4).all(): 

356 return True 

357 else: 

358 self.warning(''' 

359 Your mesh is not closed, the mass methods will not function 

360 correctly on this mesh. For more info: 

361 https://github.com/WoLpH/numpy-stl/issues/69 

362 '''.strip()) 

363 return False 

364 

365 def get_mass_properties(self): 

366 ''' 

367 Evaluate and return a tuple with the following elements: 

368 - the volume 

369 - the position of the center of gravity (COG) 

370 - the inertia matrix expressed at the COG 

371 

372 Documentation can be found here: 

373 http://www.geometrictools.com/Documentation/PolyhedralMassProperties.pdf 

374 ''' 

375 self.check() 

376 

377 def subexpression(x): 

378 w0, w1, w2 = x[:, 0], x[:, 1], x[:, 2] 

379 temp0 = w0 + w1 

380 f1 = temp0 + w2 

381 temp1 = w0 * w0 

382 temp2 = temp1 + w1 * temp0 

383 f2 = temp2 + w2 * f1 

384 f3 = w0 * temp1 + w1 * temp2 + w2 * f2 

385 g0 = f2 + w0 * (f1 + w0) 

386 g1 = f2 + w1 * (f1 + w1) 

387 g2 = f2 + w2 * (f1 + w2) 

388 return f1, f2, f3, g0, g1, g2 

389 

390 x0, x1, x2 = self.x[:, 0], self.x[:, 1], self.x[:, 2] 

391 y0, y1, y2 = self.y[:, 0], self.y[:, 1], self.y[:, 2] 

392 z0, z1, z2 = self.z[:, 0], self.z[:, 1], self.z[:, 2] 

393 a1, b1, c1 = x1 - x0, y1 - y0, z1 - z0 

394 a2, b2, c2 = x2 - x0, y2 - y0, z2 - z0 

395 d0, d1, d2 = b1 * c2 - b2 * c1, a2 * c1 - a1 * c2, a1 * b2 - a2 * b1 

396 

397 f1x, f2x, f3x, g0x, g1x, g2x = subexpression(self.x) 

398 f1y, f2y, f3y, g0y, g1y, g2y = subexpression(self.y) 

399 f1z, f2z, f3z, g0z, g1z, g2z = subexpression(self.z) 

400 

401 intg = numpy.zeros((10)) 

402 intg[0] = sum(d0 * f1x) 

403 intg[1:4] = sum(d0 * f2x), sum(d1 * f2y), sum(d2 * f2z) 

404 intg[4:7] = sum(d0 * f3x), sum(d1 * f3y), sum(d2 * f3z) 

405 intg[7] = sum(d0 * (y0 * g0x + y1 * g1x + y2 * g2x)) 

406 intg[8] = sum(d1 * (z0 * g0y + z1 * g1y + z2 * g2y)) 

407 intg[9] = sum(d2 * (x0 * g0z + x1 * g1z + x2 * g2z)) 

408 intg /= numpy.array([6, 24, 24, 24, 60, 60, 60, 120, 120, 120]) 

409 volume = intg[0] 

410 cog = intg[1:4] / volume 

411 cogsq = cog ** 2 

412 inertia = numpy.zeros((3, 3)) 

413 inertia[0, 0] = intg[5] + intg[6] - volume * (cogsq[1] + cogsq[2]) 

414 inertia[1, 1] = intg[4] + intg[6] - volume * (cogsq[2] + cogsq[0]) 

415 inertia[2, 2] = intg[4] + intg[5] - volume * (cogsq[0] + cogsq[1]) 

416 inertia[0, 1] = inertia[1, 0] = -(intg[7] - volume * cog[0] * cog[1]) 

417 inertia[1, 2] = inertia[2, 1] = -(intg[8] - volume * cog[1] * cog[2]) 

418 inertia[0, 2] = inertia[2, 0] = -(intg[9] - volume * cog[2] * cog[0]) 

419 return volume, cog, inertia 

420 

421 def update_units(self): 

422 units = self.normals.copy() 

423 non_zero_areas = self.areas > 0 

424 areas = self.areas 

425 

426 if non_zero_areas.shape[0] != areas.shape[0]: # pragma: no cover 

427 self.warning('Zero sized areas found, ' 

428 'units calculation will be partially incorrect') 

429 

430 if non_zero_areas.any(): 

431 non_zero_areas.shape = non_zero_areas.shape[0] 

432 areas = numpy.hstack((2 * areas[non_zero_areas],) * DIMENSIONS) 

433 units[non_zero_areas] /= areas 

434 

435 self.units = units 

436 

437 @classmethod 

438 def rotation_matrix(cls, axis, theta): 

439 ''' 

440 Generate a rotation matrix to Rotate the matrix over the given axis by 

441 the given theta (angle) 

442 

443 Uses the `Euler-Rodrigues 

444 <https://en.wikipedia.org/wiki/Euler%E2%80%93Rodrigues_formula>`_ 

445 formula for fast rotations. 

446 

447 :param numpy.array axis: Axis to rotate over (x, y, z) 

448 :param float theta: Rotation angle in radians, use `math.radians` to 

449 convert degrees to radians if needed. 

450 ''' 

451 axis = numpy.asarray(axis) 

452 # No need to rotate if there is no actual rotation 

453 if not axis.any(): 

454 return numpy.identity(3) 

455 

456 theta = 0.5 * numpy.asarray(theta) 

457 

458 axis = axis / numpy.linalg.norm(axis) 

459 

460 a = math.cos(theta) 

461 b, c, d = - axis * math.sin(theta) 

462 angles = a, b, c, d 

463 powers = [x * y for x in angles for y in angles] 

464 aa, ab, ac, ad = powers[0:4] 

465 ba, bb, bc, bd = powers[4:8] 

466 ca, cb, cc, cd = powers[8:12] 

467 da, db, dc, dd = powers[12:16] 

468 

469 return numpy.array([[aa + bb - cc - dd, 2 * (bc + ad), 2 * (bd - ac)], 

470 [2 * (bc - ad), aa + cc - bb - dd, 2 * (cd + ab)], 

471 [2 * (bd + ac), 2 * (cd - ab), aa + dd - bb - cc]]) 

472 

473 def rotate(self, axis, theta=0, point=None): 

474 ''' 

475 Rotate the matrix over the given axis by the given theta (angle) 

476 

477 Uses the :py:func:`rotation_matrix` in the background. 

478 

479 .. note:: Note that the `point` was accidentaly inverted with the 

480 old version of the code. To get the old and incorrect behaviour 

481 simply pass `-point` instead of `point` or `-numpy.array(point)` if 

482 you're passing along an array. 

483 

484 :param numpy.array axis: Axis to rotate over (x, y, z) 

485 :param float theta: Rotation angle in radians, use `math.radians` to 

486 convert degrees to radians if needed. 

487 :param numpy.array point: Rotation point so manual translation is not 

488 required 

489 ''' 

490 # No need to rotate if there is no actual rotation 

491 if not theta: 

492 return 

493 

494 self.rotate_using_matrix(self.rotation_matrix(axis, theta), point) 

495 

496 def rotate_using_matrix(self, rotation_matrix, point=None): 

497 ''' 

498 Rotate using a given rotation matrix and optional rotation point 

499 

500 Note that this rotation produces clockwise rotations for positive 

501 angles which is arguably incorrect but will remain for legacy reasons. 

502 For more details, read here: 

503 https://github.com/WoLpH/numpy-stl/issues/166 

504 ''' 

505 

506 identity = numpy.identity(rotation_matrix.shape[0]) 

507 # No need to rotate if there is no actual rotation 

508 if not rotation_matrix.any() or (identity == rotation_matrix).all(): 

509 return 

510 

511 if isinstance(point, (numpy.ndarray, list, tuple)) and len(point) == 3: 

512 point = numpy.asarray(point) 

513 elif point is None: 

514 point = numpy.array([0, 0, 0]) 

515 elif isinstance(point, (int, float)): 

516 point = numpy.asarray([point] * 3) 

517 else: 

518 raise TypeError('Incorrect type for point', point) 

519 

520 def _rotate(matrix): 

521 if point.any(): 

522 # Translate while rotating 

523 return (matrix - point).dot(rotation_matrix) + point 

524 else: 

525 # Simply apply the rotation 

526 return matrix.dot(rotation_matrix) 

527 

528 # Rotate the normals 

529 self.normals[:] = _rotate(self.normals[:]) 

530 

531 # Rotate the vectors 

532 for i in range(3): 

533 self.vectors[:, i] = _rotate(self.vectors[:, i]) 

534 

535 def translate(self, translation): 

536 ''' 

537 Translate the mesh in the three directions 

538 

539 :param numpy.array translation: Translation vector (x, y, z) 

540 ''' 

541 assert len(translation) == 3, "Translation vector must be of length 3" 

542 self.x += translation[0] 

543 self.y += translation[1] 

544 self.z += translation[2] 

545 

546 def transform(self, matrix): 

547 ''' 

548 Transform the mesh with a rotation and a translation stored in a 

549 single 4x4 matrix 

550 

551 :param numpy.array matrix: Transform matrix with shape (4, 4), where 

552 matrix[0:3, 0:3] represents the rotation 

553 part of the transformation 

554 matrix[0:3, 3] represents the translation 

555 part of the transformation 

556 ''' 

557 is_a_4x4_matrix = matrix.shape == (4, 4) 

558 assert is_a_4x4_matrix, "Transformation matrix must be of shape (4, 4)" 

559 rotation = matrix[0:3, 0:3] 

560 unit_det_rotation = numpy.allclose(numpy.linalg.det(rotation), 1.0) 

561 assert unit_det_rotation, "Rotation matrix has not a unit determinant" 

562 for i in range(3): 

563 self.vectors[:, i] = numpy.dot(rotation, self.vectors[:, i].T).T 

564 self.x += matrix[0, 3] 

565 self.y += matrix[1, 3] 

566 self.z += matrix[2, 3] 

567 

568 def _get_or_update(key): 

569 def _get(self): 

570 if not hasattr(self, '_%s' % key): 

571 getattr(self, 'update_%s' % key)() 

572 return getattr(self, '_%s' % key) 

573 

574 return _get 

575 

576 def _set(key): 

577 def _set(self, value): 

578 setattr(self, '_%s' % key, value) 

579 

580 return _set 

581 

582 min_ = property(_get_or_update('min'), _set('min'), 

583 doc='Mesh minimum value') 

584 max_ = property(_get_or_update('max'), _set('max'), 

585 doc='Mesh maximum value') 

586 areas = property(_get_or_update('areas'), _set('areas'), 

587 doc='Mesh areas') 

588 centroids = property(_get_or_update('centroids'), _set('centroids'), 

589 doc='Mesh centroids') 

590 units = property(_get_or_update('units'), _set('units'), 

591 doc='Mesh unit vectors') 

592 

593 def __getitem__(self, k): 

594 return self.points[k] 

595 

596 def __setitem__(self, k, v): 

597 self.points[k] = v 

598 

599 def __len__(self): 

600 return self.points.shape[0] 

601 

602 def __iter__(self): 

603 for point in self.points: 

604 yield point 

605 

606 def get_mass_properties_with_density(self, density): 

607 # add density for mesh,density unit kg/m3 when mesh is unit is m 

608 self.check() 

609 

610 def subexpression(x): 

611 w0, w1, w2 = x[:, 0], x[:, 1], x[:, 2] 

612 temp0 = w0 + w1 

613 f1 = temp0 + w2 

614 temp1 = w0 * w0 

615 temp2 = temp1 + w1 * temp0 

616 f2 = temp2 + w2 * f1 

617 f3 = w0 * temp1 + w1 * temp2 + w2 * f2 

618 g0 = f2 + w0 * (f1 + w0) 

619 g1 = f2 + w1 * (f1 + w1) 

620 g2 = f2 + w2 * (f1 + w2) 

621 return f1, f2, f3, g0, g1, g2 

622 

623 x0, x1, x2 = self.x[:, 0], self.x[:, 1], self.x[:, 2] 

624 y0, y1, y2 = self.y[:, 0], self.y[:, 1], self.y[:, 2] 

625 z0, z1, z2 = self.z[:, 0], self.z[:, 1], self.z[:, 2] 

626 a1, b1, c1 = x1 - x0, y1 - y0, z1 - z0 

627 a2, b2, c2 = x2 - x0, y2 - y0, z2 - z0 

628 d0, d1, d2 = b1 * c2 - b2 * c1, a2 * c1 - a1 * c2, a1 * b2 - a2 * b1 

629 

630 f1x, f2x, f3x, g0x, g1x, g2x = subexpression(self.x) 

631 f1y, f2y, f3y, g0y, g1y, g2y = subexpression(self.y) 

632 f1z, f2z, f3z, g0z, g1z, g2z = subexpression(self.z) 

633 

634 intg = numpy.zeros((10)) 

635 intg[0] = sum(d0 * f1x) 

636 intg[1:4] = sum(d0 * f2x), sum(d1 * f2y), sum(d2 * f2z) 

637 intg[4:7] = sum(d0 * f3x), sum(d1 * f3y), sum(d2 * f3z) 

638 intg[7] = sum(d0 * (y0 * g0x + y1 * g1x + y2 * g2x)) 

639 intg[8] = sum(d1 * (z0 * g0y + z1 * g1y + z2 * g2y)) 

640 intg[9] = sum(d2 * (x0 * g0z + x1 * g1z + x2 * g2z)) 

641 intg /= numpy.array([6, 24, 24, 24, 60, 60, 60, 120, 120, 120]) 

642 volume = intg[0] 

643 cog = intg[1:4] / volume 

644 cogsq = cog ** 2 

645 vmass = volume * density 

646 inertia = numpy.zeros((3, 3)) 

647 

648 inertia[0, 0] = (intg[5] + intg[6]) * density - vmass * ( 

649 cogsq[1] + cogsq[2]) 

650 inertia[1, 1] = (intg[4] + intg[6]) * density - vmass * ( 

651 cogsq[2] + cogsq[0]) 

652 inertia[2, 2] = (intg[4] + intg[5]) * density - vmass * ( 

653 cogsq[0] + cogsq[1]) 

654 inertia[0, 1] = inertia[1, 0] = -( 

655 intg[7] * density - vmass * cog[0] * cog[1]) 

656 inertia[1, 2] = inertia[2, 1] = -( 

657 intg[8] * density - vmass * cog[1] * cog[2]) 

658 inertia[0, 2] = inertia[2, 0] = -( 

659 intg[9] * density - vmass * cog[2] * cog[0]) 

660 

661 return volume, vmass, cog, inertia 

662