• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kip-hart / MicroStructPy / 12562396580

31 Dec 2024 05:38PM UTC coverage: 18.583%. Remained the same
12562396580

Pull #107

github

web-flow
add requests to docs reqs per snyk
Pull Request #107: CI Pipeline Fixes

978 of 5263 relevant lines covered (18.58%)

2.23 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

48.05
/src/microstructpy/meshing/polymesh.py
1
"""Polygon Meshing
2

3
This module contains the class definition for the PolyMesh class.
4

5
"""
6
# --------------------------------------------------------------------------- #
7
#                                                                             #
8
# Import Modules                                                              #
9
#                                                                             #
10
# --------------------------------------------------------------------------- #
11

12

13
from __future__ import division
12✔
14
from __future__ import print_function
12✔
15

16
import os
12✔
17
import subprocess
12✔
18
import sys
12✔
19
import tempfile
12✔
20
import warnings
12✔
21

22
import numpy as np
12✔
23
import pyvoro
12✔
24
from matplotlib import collections
12✔
25
from matplotlib import patches
12✔
26
from matplotlib import pyplot as plt
12✔
27
from mpl_toolkits.mplot3d import Axes3D
12✔
28
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
12✔
29
from scipy.spatial import distance
12✔
30

31
from microstructpy import _misc
12✔
32
from microstructpy import geometry
12✔
33

34
__all__ = ['PolyMesh']
12✔
35
__author__ = 'Kenneth (Kip) Hart'
12✔
36

37

38
# --------------------------------------------------------------------------- #
39
#                                                                             #
40
# PolyMesh Class                                                              #
41
#                                                                             #
42
# --------------------------------------------------------------------------- #
43
class PolyMesh(object):
12✔
44
    """Polygonal/Polyhedral mesh.
45

46
    The PolyMesh class contains the points, edges, regions, etc. in a polygon
47
    (2D) or polyhedron (3D) mesh.
48

49
    The points attribute is a numpy array containing the (x, y) or (x, y, z)
50
    coordinates of each point in the mesh. This is the only attribute that
51
    contains floating point numbers. The rest contain indices/integers.
52

53
    The facets attribute describes the interfaces between the polygons/
54
    polyhedra. In 2D, these interfaces are line segments and each facet
55
    contains the indices of the points at each end of the line segment. These
56
    indices are unorderd. In 3D, the interfaces are polygons so each facet
57
    contains the indices of the points on that polygon. These indices are
58
    ordered such that neighboring keypoints are connected by line segments
59
    that form the polygon.
60

61
    The regions attribute contains the area (2D) or volume (3D). In 2D, a
62
    region is given by an ordered list of facets, or edges, that enclose the
63
    polygon. In 3D, the region is given by an un-ordered list of facets,
64
    or polygons, that enclose the polyhedron.
65

66
    For each region, there is also an associated seed number and material
67
    phase. These data are stored in the seed_number and phase_number
68
    attributes, which have the same length as the regions list.
69

70
    Args:
71
        points (list or numpy.ndarray): An Nx2 or Nx3 array of coordinates
72
            in the mesh.
73
        facets (list): List of facets between regions. In 2D, this is a list
74
            of edges (Nx2). In 3D, this is a list of 3D polygons.
75
        regions (list): A list of polygons (2D) or polyhedra (3D), with each
76
            element of the list being a list of facet indices.
77
        seed_numbers (list or numpy.ndarray): *(optional)* The seed number
78
            associated with each region.
79
            Defaults to 0 for all regions.
80
        phase_numbers (list or numpy.ndarray): *(optional)* The phase number
81
            associated with each region.
82
            Defaults to 0 for all regions.
83
        facet_neighbors (list or numpy.ndarray): *(optional)* The region
84
            numbers on either side of each facet.
85
            If not givien, a neighbor list is computed from ``regions``.
86
        volumes (list or numpy.ndarray): *(optional)* The area/volume of each
87
            region.
88
            If not given, region volumes are calculated based on ``points``,
89
            ``facets``, and ``regions``.
90

91
    """
92

93
    # ----------------------------------------------------------------------- #
94
    # Constructors                                                            #
95
    # ----------------------------------------------------------------------- #
96
    def __init__(self, points, facets, regions, seed_numbers=None,
12✔
97
                 phase_numbers=None, facet_neighbors=None, volumes=None):
98

99
        self.points = points
12✔
100
        self.facets = facets
12✔
101
        self.regions = regions
12✔
102

103
        if facet_neighbors is None:
12✔
104
            # Find facet neighbors
105
            facet_neighs = [[-1, -1] for _ in facets]
12✔
106
            n_neighs = [0 for _ in facets]
12✔
107
            for r_num, region in enumerate(regions):
12✔
108
                for f_num in region:
12✔
109
                    ind = n_neighs[f_num]
12✔
110
                    facet_neighs[f_num][ind] = r_num
12✔
111
                    n_neighs[f_num] += 1
12✔
112

113
            # update negative neighbor numbers to follow the voro++
114
            # convention, described in the %n section of this website:
115
            # http://math.lbl.gov/voro++/doc/custom.html
116
            pt_arr = np.array(points)
12✔
117
            pt_mins = pt_arr.min(axis=0)
12✔
118
            pt_maxs = pt_arr.max(axis=0)
12✔
119
            for fnum, facet in enumerate(facets):
12✔
120
                if facet_neighs[fnum][-1] != -1:
12✔
121
                    continue
12✔
122

123
                f_pts = pt_arr[facet, :]
12✔
124
                min_match = np.all(np.isclose(f_pts, pt_mins), axis=0)
12✔
125
                if np.any(min_match):
12✔
126
                    ld = 3 - len(min_match)
12✔
127
                    mask = np.pad(min_match, (0, ld), 'constant',
12✔
128
                                  constant_values=(False, False))
129
                    id = np.array([-1, -3, -5])[mask][0]
12✔
130
                    facet_neighs[fnum][-1] = id
12✔
131

132
                max_match = np.all(np.isclose(f_pts, pt_maxs), axis=0)
12✔
133
                if np.any(max_match):
12✔
134
                    ld = 3 - len(max_match)
12✔
135
                    mask = np.pad(max_match, (0, ld), 'constant',
12✔
136
                                  constant_values=(False, False))
137
                    id = np.array([-2, -4, -6])[mask][0]
12✔
138
                    facet_neighs[fnum][-1] = id
12✔
139

140
            self.facet_neighbors = facet_neighs
12✔
141
        else:
142
            self.facet_neighbors = facet_neighbors
12✔
143

144
        if seed_numbers is None:
12✔
145
            self.seed_numbers = [0 for _ in regions]
12✔
146
        else:
147
            self.seed_numbers = seed_numbers
12✔
148

149
        if phase_numbers is None:
12✔
150
            self.phase_numbers = [0 for _ in regions]
12✔
151
        else:
152
            self.phase_numbers = phase_numbers
12✔
153

154
        if volumes is None:
12✔
155
            vols = np.zeros(len(self.regions))
12✔
156
            n = len(self.points[0])
12✔
157
            for i, region in enumerate(self.regions):
12✔
158
                # 'center' of region is arbitrary, since region is convex
159
                cen = np.array(self.points)[self.facets[region[0]][0]]
12✔
160
                for f_num in region:
12✔
161
                    facet = np.array(self.facets[f_num])
12✔
162
                    j_max = len(facet) - n + 2
12✔
163
                    # convert facet into (n-1)D simplices
164
                    for j in range(1, j_max):
12✔
165
                        inds = np.append(np.arange(j, j + n - 1), 0)
12✔
166
                        simplex = facet[inds]
12✔
167
                        facet_pts = np.array(self.points)[simplex]
12✔
168
                        rel_pos = facet_pts - cen
12✔
169
                        # simplex volume is |det([Dx1, Dy1; Dx2, Dy2])| in 2D
170
                        vols[i] += np.abs(np.linalg.det(rel_pos))
12✔
171

172
            # the 1/2 out front in 2D and 1/6 in 3D
173
            while n > 1:
12✔
174
                vols /= n
12✔
175
                n -= 1
12✔
176
            self.volumes = vols
12✔
177
        else:
178
            self.volumes = volumes
12✔
179

180
    # ----------------------------------------------------------------------- #
181
    # Representation and String Functions                                     #
182
    # ----------------------------------------------------------------------- #
183
    def __repr__(self):
12✔
184
        repr_str = 'PolyMesh('
12✔
185
        repr_str += repr(self.points)
12✔
186
        repr_str += ', '
12✔
187
        repr_str += repr(self.facets)
12✔
188
        repr_str += ', '
12✔
189
        repr_str += repr(self.regions)
12✔
190
        for att in ('seed_numbers', 'phase_numbers'):
12✔
191
            repr_str += ', '
12✔
192
            vals = self.__dict__[att]
12✔
193
            if all([n == 0 for n in vals]):
12✔
194
                repr_str += repr(None)
12✔
195
            else:
196
                repr_str += repr(vals)
12✔
197
        repr_str += ')'
12✔
198
        return repr_str
12✔
199

200
    def __str__(self):
12✔
201
        nv = len(self.points)
12✔
202
        nd = len(self.points[0])
12✔
203
        pt_fmt = '\t'
12✔
204
        pt_fmt += ', '.join(['{pt[' + str(i) + ']: e}' for i in range(nd)])
12✔
205

206
        str_str = 'Mesh Points: ' + str(nv) + '\n'
12✔
207
        str_str += ''.join([pt_fmt.format(pt=p) + '\n' for p in self.points])
12✔
208

209
        str_str += 'Mesh Facets: ' + str(len(self.facets)) + '\n'
12✔
210
        str_str += ''.join(['\t' + str(tuple(f))[1:-1] + '\n'
12✔
211
                            for f in self.facets])
212

213
        str_str += 'Facet Neighbors: ' + str(len(self.facet_neighbors)) + '\n'
12✔
214
        str_str += ''.join(['\t' + str(tuple(n))[1:-1] + '\n'
12✔
215
                            for n in self.facet_neighbors])
216

217
        str_str += 'Mesh Regions: ' + str(len(self.regions)) + '\n'
12✔
218
        str_str += ''.join(['\t' + str(tuple(r))[1:-1] + '\n'
12✔
219
                            for r in self.regions])
220

221
        str_str += 'Seed Numbers: ' + str(len(self.seed_numbers)) + '\n'
12✔
222
        str_str += ''.join(['\t' + str(n) + '\n' for n in self.seed_numbers])
12✔
223

224
        str_str += 'Phase Numbers: ' + str(len(self.phase_numbers)) + '\n'
12✔
225
        str_str += ''.join(['\t' + str(n) + '\n' for n in self.phase_numbers])
12✔
226

227
        str_str += 'Volumes: ' + str(len(self.volumes)) + '\n'
12✔
228
        str_str += '\n'.join(['\t' + str(v) for v in self.volumes])
12✔
229
        return str_str
12✔
230

231
    # ----------------------------------------------------------------------- #
232
    # Read and Write Functions                                                #
233
    # ----------------------------------------------------------------------- #
234
    def write(self, filename, format='txt'):
12✔
235
        """Write the mesh to a file.
236

237
        This function writes the polygon/polyhedron mesh to a file.
238
        See the :ref:`s_poly_file_io` section of the
239
        :ref:`c_file_formats` guide for more information about the available
240
        output file formats.
241

242
        Args:
243
            filename (str): Name of the file to be written.
244
            format (str): *(optional)* {'txt' | 'poly' | 'ply' | 'vtk' }
245
                Format of the data in the file. Defaults to ``'txt'``.
246

247
        """
248
        if format in ('str', 'txt'):
12✔
249
            with open(filename, 'w') as f:
12✔
250
                f.write(str(self) + '\n')
12✔
251

252
        elif format == 'poly':
×
253
            nv = len(self.points)
×
254
            nd = len(self.points[0])
×
255
            nf = len(self.facets)
×
256
            assert nd == 2
×
257

258
            poly = '# Polygon Mesh\n'
×
259
            poly += ' '.join([str(n) for n in (nv, 2, 0, 0)]) + '\n'
×
260

261
            # vertices
262
            poly += '# Vertices\n'
×
263
            poly += ''.join([str(i) + ''.join([' {: e}'.format(x) for x in pt])
×
264
                             + '\n' for i, pt in enumerate(self.points)])
265

266
            # facets
267
            poly += '# Segments\n'
×
268
            poly += ' '.join([str(n) for n in (nf, 0)]) + '\n'
×
269
            poly += ''.join([' '.join([str(n) for n in (nv + i, k1, k2)])
×
270
                             + '\n' for i, (k1, k2) in enumerate(self.facets)])
271

272
        elif format == 'ply':
×
273
            nv = len(self.points)
×
274
            nd = len(self.points[0])
×
275
            nf = len(self.facets)
×
276
            nr = len(self.regions)
×
277
            assert nd <= 3
×
278
            
279
            # Force 3D points 
280
            pts = np.zeros((nv, 3))
×
281
            pts[:, :nd] = self.points
×
282
            axes = ['x', 'y', 'z']
×
283

284
            # header
285
            ply = 'ply\n'
×
286
            ply += 'format ascii 1.0\n'
×
287
            ply += 'element vertex ' + str(nv) + '\n'
×
288
            ply += ''.join(['property float32 ' + a + '\n' for a in axes])
×
289
            if nd == 2:
×
290
                n_faces = nr
×
291
            else:
292
                n_faces = nf
×
293
            ply += 'element face {}\n'.format(n_faces)
×
294
            ply += 'property list uchar int vertex_indices\n'
×
295
            ply += 'end_header\n'
×
296

297
            # vertices
298
            ply += ''.join([' '.join(['{: e}'.format(x) for x in pt]) + '\n'
×
299
                            for pt in pts])
300

301
            # faces
302
            if nd == 2:  # regions -> faces
×
303
                facets = np.array(self.facets)
×
304
                ply += ''.join([str(len(r)) + ''.join([' ' + str(kp) for kp in
×
305
                                                       kp_loop(facets[r])])
306
                                + '\n' for r in self.regions])
307

308
            else:  # facets -> faces
309
                ply += ''.join([str(len(f)) + ''.join([' ' + str(kp)
×
310
                                                       for kp in f])
311
                                + '\n' for f in self.facets])
312

313
            with open(filename, 'w') as f:
×
314
                f.write(ply)
×
315

316
        elif format == 'vtk':
×
317
            vtk_s = '# vtk DataFile Version 2.0\n'
×
318
            vtk_s += 'Polygonal Mesh\n'
×
319
            vtk_s += 'ASCII\n'
×
320
            vtk_s += 'DATASET UNSTRUCTURED_GRID\n'
×
321
            if len(self.points[0]) == 2:
×
322
                # Points
323
                vtk_s += 'POINTS {} float\n'.format(len(self.points))
×
324
                for pt in self.points:
×
325
                    vtk_s += ' '.join(['{: e}'.format(x) for x in pt]) + ' 0\n'
×
326
                vtk_s += '\n'
×
327

328
                # Cells
329
                n_cells = len(self.regions)
×
330
                n_data_total = 0
×
331
                cells = 'CELLS {}'.format(n_cells) + ' {}\n'
×
332
                pts = np.array(self.points)
×
333
                for facets in self.regions:
×
334
                    vloop = kp_loop([self.facets[f] for f in facets])
×
335
                    n_kp = len(vloop)
×
336

337
                    v1 = pts[vloop[1]] - pts[vloop[0]]
×
338
                    v2 = pts[vloop[2]] - pts[vloop[0]]
×
339
                    cross_p = np.cross(v1, v2)
×
340

341
                    cells += '{} '.format(n_kp)
×
342
                    if cross_p > 0:
×
343
                        cells += ' '.join([str(kp) for kp in vloop])
×
344
                    else:
345
                        cells += ' '.join([str(kp) for kp in vloop[::-1]])
×
346
                    cells += '\n'
×
347
                    n_data_total += 1 + n_kp
×
348
                vtk_s += cells.format(n_data_total)
×
349

350
                # Cell Types
351
                vtk_s += 'CELL_TYPES {}\n'.format(n_cells)
×
352
                vtk_s += ''.join(n_cells * ['7\n'])
×
353

354
            else:
355
                # Points
356
                vtk_s += 'POINTS {} float\n'.format(len(self.points))
×
357
                for pt in self.points:
×
358
                    vtk_s += ' '.join(['{: e}'.format(x) for x in pt]) + '\n'
×
359
                vtk_s += '\n'
×
360

361
                # Cells
362
                n_cells = len(self.regions)
×
363
                cells = 'CELLS {} '.format(n_cells) + '{}\n'
×
364
                n_data_total = 0
×
365
                pts = np.array(self.points)
×
366
                for facets in self.regions:
×
367
                    # Get region center
368
                    kps = list({kp for f in facets for kp in self.facets[f]})
×
369
                    cen = pts[kps].mean(axis=0)  # estimate of center
×
370

371
                    # Write facets
372
                    n_data_region = 1
×
373
                    line = '{} ' + str(len(facets))
×
374
                    for f_num in facets:
×
375
                        facet = self.facets[f_num]
×
376
                        f_len = len(facet)
×
377

378
                        # Determine clockwise or counter-clockwise
379
                        v1 = pts[facet[1]] - pts[facet[0]]
×
380
                        v2 = pts[facet[2]] - pts[facet[0]]
×
381
                        norm_vec = np.cross(v1, v2)
×
382
                        cen_rel = cen - pts[facet[0]]
×
383
                        dot_p = np.dot(norm_vec, cen_rel)
×
384

385
                        line += ' {} '.format(f_len)
×
386
                        if dot_p < 0:
×
387
                            line += ' '.join([str(kp) for kp in facet])
×
388
                        else:
389
                            line += ' '.join([str(kp) for kp in facet[::-1]])
×
390
                        n_data_region += 1 + f_len
×
391
                    line += '\n'
×
392
                    cells += line.format(n_data_region)
×
393
                    n_data_total += 1 + n_data_region
×
394
                vtk_s += cells.format(n_data_total)
×
395
                vtk_s += '\n'
×
396

397
                # Cell Types
398
                vtk_s += 'CELL_TYPES {}\n'.format(n_cells)
×
399
                vtk_s += ''.join(n_cells * ['42\n'])
×
400

401
            # Cell Data
402
            vtk_s += '\nCELL_DATA ' + str(n_cells) + '\n'
×
403
            vtk_s += 'SCALARS seed int 1 \n'
×
404
            vtk_s += 'LOOKUP_TABLE seed\n'
×
405
            vtk_s += ''.join([str(a) + '\n' for a in self.seed_numbers])
×
406

407
            vtk_s += 'SCALARS phase int 1 \n'
×
408
            vtk_s += 'LOOKUP_TABLE phase\n'
×
409
            vtk_s += ''.join([str(a) + '\n' for a in self.phase_numbers])
×
410

411
            vtk_s += 'SCALARS volume float 1 \n'
×
412
            vtk_s += 'LOOKUP_TABLE volume\n'
×
413
            vtk_s += ''.join([str(a) + '\n' for a in self.volumes])
×
414

415
            with open(filename, 'w') as file:
×
416
                file.write(vtk_s)
×
417

418
        else:
419
            e_str = 'Cannot understand format string ' + str(format) + '.'
×
420
            raise ValueError(e_str)
×
421

422
    @classmethod
12✔
423
    def from_file(cls, filename):
12✔
424
        """Read PolyMesh from file.
425

426
        This function reads in a polygon mesh from a file and creates an
427
        instance from that file. Currently the only supported file type
428
        is the output from :meth:`.write` with the ``format='txt'`` option.
429

430
        Args:
431
            filename (str): Name of file to read from.
432

433
        Returns:
434
            PolyMesh: The instance of the class written to the file.
435

436
        """
437
        with open(filename, 'r') as file:
12✔
438
            stage = 0
12✔
439
            pts = []
12✔
440
            facets = []
12✔
441
            f_neighbors = []
12✔
442
            regions = []
12✔
443
            seed_numbers = []
12✔
444
            phase_numbers = []
12✔
445
            volumes = []
12✔
446
            for line in file.readlines():
12✔
447
                if 'Mesh Points'.lower() in line.lower():
12✔
448
                    n_pts = int(line.split(':')[1])
12✔
449
                    stage = 'points'
12✔
450
                elif 'Mesh Facets'.lower() in line.lower():
12✔
451
                    n_fts = int(line.split(':')[1])
12✔
452
                    stage = 'facets'
12✔
453
                elif 'Facet Neighbors'.lower() in line.lower():
12✔
454
                    n_nns = int(line.split(':')[1])
12✔
455
                    stage = 'facet neighbors'
12✔
456
                elif 'Mesh Regions'.lower() in line.lower():
12✔
457
                    n_rns = int(line.split(':')[1])
12✔
458
                    stage = 'regions'
12✔
459
                elif 'Seed Numbers'.lower() in line.lower():
12✔
460
                    n_sns = int(line.split(':')[1])
12✔
461
                    stage = 'seed numbers'
12✔
462
                elif 'Phase Numbers'.lower() in line.lower():
12✔
463
                    n_pns = int(line.split(':')[1])
12✔
464
                    stage = 'phase numbers'
12✔
465
                elif 'Volumes'.lower() in line.lower():
12✔
466
                    n_vols = int(line.split(':')[1])
12✔
467
                    stage = 'volumes'
12✔
468
                else:
469
                    if stage == 'points':
12✔
470
                        pts.append([float(x) for x in line.split(',')])
12✔
471
                    elif stage == 'facets':
12✔
472
                        facets.append([int(kp) for kp in line.split(',')])
12✔
473
                    elif stage == 'facet neighbors':
12✔
474
                        f_neighbors.append([int(n) for n in line.split(',')])
12✔
475
                    elif stage == 'regions':
12✔
476
                        regions.append([int(f) for f in line.split(',')])
12✔
477
                    elif stage == 'seed numbers':
12✔
478
                        seed_numbers.append(_misc.from_str(line))
12✔
479
                    elif stage == 'phase numbers':
12✔
480
                        phase_numbers.append(_misc.from_str(line))
12✔
481
                    elif stage == 'volumes':
12✔
482
                        volumes.append(_misc.from_str(line))
12✔
483
                    else:
484
                        pass
9✔
485

486
        # check the inputs
487
        assert len(pts) == n_pts
12✔
488
        assert len(facets) == n_fts
12✔
489
        assert len(regions) == n_rns
12✔
490
        assert len(seed_numbers) == n_sns
12✔
491
        assert len(phase_numbers) == n_pns
12✔
492
        assert len(volumes) == n_vols
12✔
493

494
        if len(f_neighbors) == 0:
12✔
495
            f_neighbors = None
×
496
        else:
497
            assert len(f_neighbors) == n_nns
12✔
498

499
        return cls(pts, facets, regions, seed_numbers, phase_numbers,
12✔
500
                   volumes=volumes, facet_neighbors=f_neighbors)
501

502
    # ----------------------------------------------------------------------- #
503
    # Construct from Seed List                                                #
504
    # ----------------------------------------------------------------------- #
505
    @classmethod
12✔
506
    def from_seeds(cls, seedlist, domain, edge_opt=False, n_iter=100,
12✔
507
                   verbose=False):
508
        """Create from :class:`.SeedList` and a domain.
509

510
        This function creates a polygon/polyhedron mesh from a seed list and
511
        a domain. It relies on the pyvoro package, which wraps `Voro++`_.
512
        The mesh is a Voronoi power diagram / Laguerre tessellationself.
513

514
        The pyvoro package operates on rectangular domains, so other domains
515
        are meshed in 2D by meshing in a bounding box then the boundary cells
516
        are clipped to the domain boundary.
517
        Currently non-rectangular domains in 3D are not supported.
518

519
        This function also includes the option to maximize the shortest edges
520
        in the polygonal/polyhedral mesh. Short edges cause numerical
521
        issues in finite element analysis - setting `edge_opt` to True can
522
        improve mesh quality with minimal changes to the microstructure.
523

524
        Args:
525
            seedlist (SeedList): A list of seeds in the microstructure.
526
            domain (from :mod:`microstructpy.geometry`): The domain to be
527
                filled by the seed.
528
            edge_opt (bool): *(optional)* This option will maximize the minimum
529
                edge length in the PolyMesh. The seeds associated with the
530
                shortest edge are displaced randomly to find improvement and
531
                this process iterates until `n_iter` attempts have been made
532
                for a given edge. Defaults to False.
533
            n_iter (int): *(optional)* Maximum number of iterations per edge
534
                during optimization. Ignored if `edge_opt` set to False.
535
                Defaults to 100.
536
            verbose (bool): *(optional)* Print status of edge optimization to
537
                screen. Defaults to False.
538

539
        Returns:
540
            PolyMesh: A polygon/polyhedron mesh.
541

542
        .. _`Voro++`: http://math.lbl.gov/voro++/
543

544
        """
545
        # Collect all breakdowns
546
        bkdwn2seed = np.array([], dtype='int')
12✔
547
        bkdwns = np.array([])
12✔
548
        for seed_num, seed in enumerate(seedlist):
12✔
549
            if len(seed.breakdown) == 0:
12✔
550
                seed.update_breakdown()
×
551
            bkdwn = np.array(seed.breakdown).reshape(-1, domain.n_dim + 1)
12✔
552
            in_mask = domain.within(bkdwn[:, :-1])
12✔
553
            breakdown = bkdwn[in_mask]
12✔
554

555
            m, n = breakdown.shape
12✔
556
            bkdwns = np.concatenate((bkdwns.reshape(-1, n), breakdown))
12✔
557
            bkdwn2seed = np.append(bkdwn2seed, np.full(m, seed_num))
12✔
558

559
        n_pts = bkdwns.shape[0]
12✔
560
        n_dim = bkdwns.shape[1] - 1
12✔
561

562
        # modify point list and boundaries if necessary
563
        geom = {2: geometry.Rectangle, 3: geometry.Box}
12✔
564
        voro_dom = geom[n_dim](limits=domain.limits)
12✔
565

566
        # get domain limits
567
        lims = voro_dom.limits
12✔
568

569
        # clip points from voro domain
570
        flag_val = min(bkdwn2seed) - 1
12✔
571
        n_pad = bkdwns.shape[0] - n_pts
12✔
572
        bkdwn2seed = np.pad(bkdwn2seed, (0, n_pad), 'constant',
12✔
573
                            constant_values=(0, flag_val))
574

575
        cens = bkdwns[:, :-1]
12✔
576
        rads = bkdwns[:, -1]
12✔
577

578
        # get block size
579
        sz = 2 * max(rads)
12✔
580
        if np.isclose(sz, 0):
12✔
581
            sz = 0.1 * np.min([ub - lb for lb, ub in lims])
×
582

583
        # remove extraneous breakdowns
584
        removing_pts = True
12✔
585
        while removing_pts:
12✔
586
            # Create a temporary file to run pyvoro
587
            call_str = 'import pyvoro\n\n'
12✔
588

589
            call_str += 'pts = ['
12✔
590
            beg_str = ',\n' + len('pts = [') * ' '
12✔
591
            call_str += beg_str.join([str(np.array(p).tolist()) for p in cens])
12✔
592
            call_str += ']\n\n'
12✔
593

594
            call_str += 'lims = ' + str(np.array(lims).tolist()) + '\n\n'
12✔
595

596
            call_str += 'sz = ' + str(sz) + '\n\n'
12✔
597

598
            call_str += 'rads = ['
12✔
599
            beg_str = ',\n' + len('rads = [') * ' '
12✔
600
            call_str += beg_str.join([str(rad) for rad in rads])
12✔
601
            call_str += ']\n\n'
12✔
602

603
            call_str += 'pyvoro.compute_'
12✔
604
            if n_dim == 2:
12✔
605
                call_str += '2d_'
12✔
606
            call_str += 'voronoi(pts, lims, sz, rads)\n'
12✔
607

608
            file = tempfile.NamedTemporaryFile(mode='w', suffix='.py',
12✔
609
                                               delete=False)
610
            file.write(call_str)
12✔
611
            call_filename = file.name
12✔
612
            file.close()
12✔
613

614
            # Run pyvoro
615
            p = subprocess.Popen([sys.executable, call_filename],
12✔
616
                                 stdout=subprocess.PIPE,
617
                                 stderr=subprocess.PIPE)
618
            p_out, _ = p.communicate()
12✔
619
            try:
12✔
620
                p.terminate()
12✔
621
            except OSError:
×
622
                pass
×
623

624
            os.remove(call_filename)
12✔
625

626
            # if there is output, remove those cells from the list
627
            out_str = p_out.decode('utf-8')
12✔
628
            if out_str:
12✔
629
                inds = [int(s) for s in out_str.split(':')[-1].split()]
×
630
                mask = np.full(len(rads), True)
×
631
                mask[inds] = False
×
632
                cens = cens[mask]
×
633
                rads = rads[mask]
×
634
                bkdwn2seed = bkdwn2seed[mask]
×
635
            else:
636
                removing_pts = False
12✔
637

638
        missing_seeds = set(range(len(seedlist))) - set(bkdwn2seed)
12✔
639
        assert not missing_seeds, str(missing_seeds)
12✔
640
        # compute voronoi diagram
641
        voro_fun = {2: pyvoro.compute_2d_voronoi,
12✔
642
                    3: pyvoro.compute_voronoi}[n_dim]
643
        voro = voro_fun(cens, lims, sz, rads)
12✔
644

645
        # Get only the cells within the domain
646
        cell_mask = np.full(len(bkdwn2seed), True, dtype='bool')
12✔
647
        rect_doms = ['square', 'cube', 'rectangle', 'box', 'nbox']
12✔
648
        if type(domain).__name__.lower() not in rect_doms:
12✔
649
            for cell_num, cell in enumerate(voro):
×
650
                cell_pts = np.array(cell['vertices'])
×
651
                cell_mask[cell_num] = np.any(domain.within(cell_pts))
×
652
        bkdwn2seed = bkdwn2seed[cell_mask]
12✔
653

654
        new_cell_nums = np.full(len(cell_mask), -1, dtype='int')
12✔
655
        new_cell_nums[cell_mask] = np.arange(np.sum(cell_mask))
12✔
656

657
        reduced_voro = []
12✔
658
        for old_cell_num, cell in enumerate(voro):
12✔
659
            # update the numbers of adjacent cells
660
            faces = cell['faces']
12✔
661
            for face in faces:
12✔
662
                old_adj_cell_num = face['adjacent_cell']
12✔
663
                if old_adj_cell_num >= 0:
12✔
664
                    new_adj_cell_num = new_cell_nums[old_adj_cell_num]
12✔
665
                    face['adjacent_cell'] = new_adj_cell_num
12✔
666
            cell['faces'] = faces
12✔
667

668
            # add cell to voro
669
            if cell_mask[old_cell_num]:
12✔
670
                reduced_voro.append(cell)
12✔
671

672
        # Clip cells to domain
673
        voro = [_clip_cell(c, domain) for c in reduced_voro]
12✔
674

675
        # create global key point and facet lists
676
        pts_global = []
12✔
677
        pts_conn = []
12✔
678
        local_kp_conn = {}
12✔
679

680
        for cell_num, cell_data in enumerate(voro):
12✔
681
            pts_local = cell_data['vertices']
12✔
682

683
            for face_data in cell_data['faces']:
12✔
684
                adj_cell = face_data['adjacent_cell']
12✔
685
                simplex_local = face_data['vertices']
12✔
686
                for kp_local in simplex_local:
12✔
687
                    key = (cell_num, kp_local)
12✔
688
                    if key in local_kp_conn:
12✔
689
                        kp_global = local_kp_conn[key]
12✔
690

691
                    else:
692
                        kp_global = len(pts_global)
12✔
693
                        pt = pts_local[kp_local]
12✔
694
                        pts_global.append(pt)
12✔
695
                        pts_conn.append({cell_num: kp_local})
12✔
696
                        local_kp_conn[key] = kp_global
12✔
697

698
                    if (adj_cell >= 0) and (adj_cell < len(voro)):
12✔
699
                        conn_info = pts_conn[kp_global]
12✔
700
                        if adj_cell not in conn_info:
12✔
701
                            adj_cell_data = voro[adj_cell]
12✔
702
                            adj_pts_local = np.array(adj_cell_data['vertices'])
12✔
703
                            rel_pos = adj_pts_local - pts_global[kp_global]
12✔
704
                            sq_dist = np.sum(rel_pos * rel_pos, axis=-1)
12✔
705
                            adj_kp_local = np.argmin(sq_dist)
12✔
706

707
                            adj_key = (adj_cell, adj_kp_local)
12✔
708
                            local_kp_conn[adj_key] = kp_global
12✔
709
                            pts_conn[kp_global][adj_cell] = adj_kp_local
12✔
710

711
        # create facet and region lists
712
        facet_list = []
12✔
713
        facet_neighbor_list = []
12✔
714
        region_list = [[] for cell in voro]
12✔
715
        for cell_num, cell_data in enumerate(voro):
12✔
716
            for face_data in cell_data['faces']:
12✔
717
                adj_cell_num = face_data['adjacent_cell']
12✔
718
                if adj_cell_num >= len(voro):
12✔
719
                    adj_cell_num = -1
×
720

721
                if adj_cell_num < cell_num:
12✔
722
                    neighbor_pair = (adj_cell_num, cell_num)
12✔
723
                    s_lcl = face_data['vertices']
12✔
724
                    s_glbl = [local_kp_conn[(cell_num, kp)] for kp in s_lcl]
12✔
725
                    if adj_cell_num < 0:
12✔
726
                        pts_f = [pts_global[kp] for kp in s_glbl]
12✔
727
                        if not _is_outward(pts_f, adj_cell_num):
12✔
728
                            s_glbl.reverse()
12✔
729

730
                    face_num = len(facet_list)
12✔
731
                    facet_list.append(s_glbl)
12✔
732
                    facet_neighbor_list.append(neighbor_pair)
12✔
733

734
                    for f_cell_num in neighbor_pair:
12✔
735
                        if (f_cell_num >= 0) and (f_cell_num < len(voro)):
12✔
736
                            region_list[f_cell_num].append(face_num)
12✔
737

738
        # create phase number list
739
        phase_nums = [seedlist[i].phase for i in bkdwn2seed]
12✔
740

741
        # Create volume list
742
        vols = [cell['volume'] for cell in voro]
12✔
743

744
        # Create initial mesh
745
        pmesh = cls(pts_global, facet_list, region_list, bkdwn2seed,
12✔
746
                    phase_nums, facet_neighbor_list, vols)
747

748
        # short edge optimization
749
        if edge_opt:
12✔
750
            seed2bkdwn = {i: [] for i in range(len(seedlist))}
×
751
            for i, n in enumerate(pmesh.seed_numbers):
×
752
                seed2bkdwn[n].append(i)
×
753

754
            # Find the shorted edge
755
            edge_lens = _edge_lengths(pmesh)
×
756
            min_edge = _shortest_edge(edge_lens)
×
757
            min_len = edge_lens[min_edge]['length']
×
758

759
            # Format verbose print string
760
            n_kps = len(pmesh.points)
×
761
            n_kp_space = int(np.log10(n_kps)) + 1
×
762
            n_iter_space = int(np.log10(n_iter))
×
763
            v_fmt = 'min length: {0:.3e} | '
×
764
            v_fmt += 'edge: {1[0]:' + str(n_kp_space) + 'd}, '
×
765
            v_fmt += '{1[1]:' + str(n_kp_space) + 'd} | '
×
766
            v_fmt += 'n iter: {2:' + str(n_iter_space) + 'd} / '
×
767
            v_fmt += str(n_iter)
×
768

769
            i_n_attempts = 0
×
770
            while i_n_attempts < n_iter:
×
771
                print(v_fmt.format(min_len, min_edge, i_n_attempts))
×
772
                # Create Displacement
773
                max_step_size = float('inf')
×
774
                step_fracs = 2 * np.random.rand(3) - 1  # [-1, 1]
×
775
                new_cens = np.copy(cens)
×
776

777
                e_neighs = edge_lens[min_edge]['regions']
×
778
                edge_pts = np.array(pmesh.points)[list(min_edge)]
×
779
                for region_num in e_neighs:
×
780
                    if region_num >= 0:
×
781
                        step_size = 0.1 * rads[region_num]
×
782
                        max_step_size = min(max_step_size, step_size)
×
783
                for f, region_num in zip(step_fracs, e_neighs):
×
784
                    if region_num >= 0:
×
785
                        e_norm_vec = _point_line_vec(cens[region_num],
×
786
                                                     edge_pts)
787
                        step = f * max_step_size * e_norm_vec
×
788
                        new_cens[region_num] += step
×
789

790
                # Update Seeds
791
                new_bkdwns = [list(c) + [r] for c, r in zip(new_cens, rads)]
×
792
                for i, seed in enumerate(seedlist):
×
793
                    seed.breakdown = [new_bkdwns[j] for j in seed2bkdwn[i]]
×
794

795
                # Create New Polygonal Mesh
796
                try:
×
797
                    new_pmesh = cls.from_seeds(seedlist, domain,
×
798
                                               edge_opt=False)
799
                except AssertionError:
×
800
                    i_n_attempts += 1
×
801
                    continue
×
802

803
                new_edge_lens = _edge_lengths(new_pmesh)
×
804
                new_min_edge = _shortest_edge(new_edge_lens)
×
805
                new_min_len = new_edge_lens[new_min_edge]['length']
×
806

807
                if new_min_len > min_len:
×
808
                    if new_min_edge != min_edge:
×
809
                        i_n_attempts = 0
×
810
                    else:
811
                        i_n_attempts += 1
×
812
                    edge_lens = new_edge_lens
×
813
                    pmesh = new_pmesh
×
814
                    min_len = new_min_len
×
815
                    min_edge = new_min_edge
×
816
                    cens = new_cens
×
817
                else:
818
                    i_n_attempts += 1
×
819
        return pmesh
12✔
820

821
    # ----------------------------------------------------------------------- #
822
    # Plot Mesh                                                               #
823
    # ----------------------------------------------------------------------- #
824
    def plot(self, index_by='seed', material=[], loc=0, **kwargs):
12✔
825
        """Plot the mesh.
826

827
        This function plots the polygon mesh.
828
        In 2D, this creates a class:`matplotlib.collections.PolyCollection`
829
        and adds it to the current axes.
830
        In 3D, it creates a
831
        :class:`mpl_toolkits.mplot3d.art3d.Poly3DCollection` and
832
        adds it to the current axes.
833
        The keyword arguments are passed though to matplotlib.
834

835
        Args:
836
            index_by (str): *(optional)* {'facet' | 'material' | 'seed'}
837
                Flag for indexing into the other arrays passed into the
838
                function. For example,
839
                ``plot(index_by='material', color=['blue', 'red'])`` will plot
840
                the regions with ``phase_number`` equal to 0 in blue, and
841
                regions with ``phase_number`` equal to 1 in red. The facet
842
                option is only available for 3D plots. Defaults to 'seed'.
843
            material (list): *(optional)* Names of material phases. One entry
844
                per material phase (the ``index_by`` argument is ignored).
845
                If this argument is set, a legend is added to the plot with
846
                one entry per material.
847
            loc (int or str): *(optional)* The location of the legend,
848
                if 'material' is specified. This argument is passed directly
849
                through to :func:`matplotlib.pyplot.legend`. Defaults to 0,
850
                which is 'best' in matplotlib.
851
            **kwargs: Keyword arguments for matplotlib.
852

853
        """
854
        n_dim = len(self.points[0])
×
855
        if n_dim == 2 or plt.gca().axes:
×
856
            ax = plt.gca()
×
857
        else:
858
            ax = plt.gcf().add_subplot(projection=Axes3D.name)
×
859
        n_obj = _misc.ax_objects(ax)
×
860
        if n_obj > 0:
×
861
            xlim = ax.get_xlim()
×
862
            ylim = ax.get_ylim()
×
863
        else:
864
            xlim = [float('inf'), -float('inf')]
×
865
            ylim = [float('inf'), -float('inf')]
×
866
        if n_dim == 2:
×
867
            # create vertex loops for each poly
868
            vloops = [kp_loop([self.facets[f] for f in r]) for r in
×
869
                      self.regions]
870
            # create poly input
871
            xy = [np.array([self.points[kp] for kp in lp]) for lp in vloops]
×
872

873
            plt_kwargs = {}
×
874
            for key, value in kwargs.items():
×
875
                if type(value) in (list, np.array):
×
876
                    plt_value = []
×
877
                    for s, p in zip(self.seed_numbers, self.phase_numbers):
×
878
                        if index_by == 'material':
×
879
                            region_value = value[p]
×
880
                        elif index_by == 'seed':
×
881
                            region_value = value[s]
×
882
                        else:
883
                            e_str = 'Cannot index by {}.'.format(index_by)
×
884
                            raise ValueError(e_str)
×
885
                        plt_value.append(region_value)
×
886
                else:
887
                    plt_value = value
×
888
                plt_kwargs[key] = plt_value
×
889
            pc = collections.PolyCollection(xy, **plt_kwargs)
×
890
            ax.add_collection(pc)
×
891
            ax.autoscale_view()
×
892
        elif n_dim == 3:
×
893
            if n_obj > 0:
×
894
                zlim = ax.get_zlim()
×
895
            else:
896
                zlim = [float('inf'), -float('inf')]
×
897
            self.plot_facets(index_by=index_by, **kwargs)
×
898

899
        else:
900
            raise NotImplementedError('Cannot plot in ' + str(n_dim) + 'D.')
×
901

902
        # Add legend
903
        if material and index_by in ('seed', 'material'):
×
904
            p_kwargs = [{'label': m} for m in material]
×
905
            s2p = {s: p for s, p in zip(self.seed_numbers, self.phase_numbers)}
×
906
            for key, value in kwargs.items():
×
907
                if type(value) in (list, np.array):
×
908
                    if index_by == 'material':
×
909
                        for p, v in enumerate(value):
×
910
                            p_kwargs[p][key] = v
×
911
                    else:
912
                        for s, v in enumerate(value):
×
913
                            p = s2p[s]
×
914
                            p_kwargs[p][key] = v
×
915
                else:
916
                    for i, m in enumerate(material):
×
917
                        p_kwargs[i][key] = value
×
918

919
            # Replace plural keywords
920
            for p_kw in p_kwargs:
×
921
                for kw in _misc.mpl_plural_kwargs:
×
922
                    if kw in p_kw:
×
923
                        p_kw[kw[:-1]] = p_kw[kw]
×
924
                        del p_kw[kw]
×
925
            handles = [patches.Patch(**p_kw) for p_kw in p_kwargs]
×
926
            ax.legend(handles=handles, loc=loc)
×
927

928
        # Adjust Axes
929
        mins = np.array(self.points).min(axis=0)
×
930
        maxs = np.array(self.points).max(axis=0)
×
931
        xlim = (min(xlim[0], mins[0]), max(xlim[1], maxs[0]))
×
932
        ylim = (min(ylim[0], mins[1]), max(ylim[1], maxs[1]))
×
933
        if n_dim == 2:
×
934
            plt.axis('square')
×
935
            plt.xlim(xlim)
×
936
            plt.ylim(ylim)
×
937
        elif n_dim == 3:
×
938
            zlim = (min(zlim[0], mins[2]), max(zlim[1], maxs[2]))
×
939
            ax.set_xlim(xlim)
×
940
            ax.set_ylim(ylim)
×
941
            ax.set_zlim(zlim)
×
942

943
            _misc.axisEqual3D(ax)
×
944

945
    def plot_facets(self, index_by='seed', hide_interior=True, **kwargs):
12✔
946
        """Plot PolyMesh facets.
947

948
        This function plots the facets of the polygon mesh, rather than the
949
        regions.
950
        In 2D, it adds a :class:`matplotlib.collections.LineCollection` to the
951
        current axes.
952
        In 3D, it adds a
953
        :class:`mpl_toolkits.mplot3d.art3d.Poly3DCollection`
954
        with ``facecolors='none'``.
955
        The keyword arguments are passed though to matplotlib.
956

957
        Args:
958
            index_by (str): *(optional)* {'facet' | 'material' | 'seed'}
959
                Flag for indexing into the other arrays passed into the
960
                function. For example,
961
                ``plot(index_by='material', color=['blue', 'red'])`` will plot
962
                the regions with ``phase_number`` equal to 0 in blue, and
963
                regions with ``phase`` equal to 1 in red. The facet option is
964
                only available for 3D plots. Defaults to 'seed'.
965
            hide_interior (bool): If True, removes interior facets from the
966
                output plot. This avoids occasional matplotlib issue where
967
                interior facets are shown in output plots.
968
            **kwargs (dict): Keyword arguments for matplotlib.
969

970
        """
971
        f_kwargs = {}
×
972
        for key, value in kwargs.items():
×
973
            if type(value) in (list, np.array):
×
974
                f_values = []
×
975
                for fn in range(len(self.facets)):
×
976
                    neighs = self.facet_neighbors[fn]
×
977
                    r = max(neighs)
×
978
                    sn = self.seed_numbers[r]
×
979
                    pn = self.phase_numbers[r]
×
980
                    if index_by == 'facet':
×
981
                        ind = fn
×
982
                    elif index_by == 'material':
×
983
                        ind = pn
×
984
                    elif index_by == 'seed':
×
985
                        ind = sn
×
986
                    else:
987
                        e_str = 'Cannot index by {}.'.format(index_by)
×
988
                        raise ValueError(e_str)
×
989
                    v = value[ind]
×
990
                    f_values.append(v)
×
991
                f_kwargs[key] = f_values
×
992
            else:
993
                f_kwargs[key] = value
×
994

995
        n_dim = len(self.points[0])
×
996
        if n_dim == 2 or plt.gcf().axes:
×
997
            ax = plt.gca()
×
998
        else:
999
            ax = plt.gcf().add_subplot(projection=Axes3D.name)
×
1000
        n_obj = _misc.ax_objects(ax)
×
1001
        if n_obj > 0:
×
1002
            xlim = ax.get_xlim()
×
1003
            ylim = ax.get_ylim()
×
1004
        else:
1005
            xlim = [float('inf'), -float('inf')]
×
1006
            ylim = [float('inf'), -float('inf')]
×
1007
        if n_dim == 2:
×
1008
            xy = [np.array([self.points[kp] for kp in f]) for f in self.facets]
×
1009

1010
            pc = collections.LineCollection(xy, **f_kwargs)
×
1011
            ax.add_collection(pc)
×
1012
            ax.autoscale_view()
×
1013
        else:
1014

1015
            if ax.has_data:
×
1016
                zlim = ax.get_zlim()
×
1017
            else:
1018
                zlim = [float('inf'), -float('inf')]
×
1019

1020
            if hide_interior:
×
1021
                f_mask = [min(fn) < 0 for fn in self.facet_neighbors]
×
1022
                xy = [np.array([self.points[kp] for kp in f]) for m, f in
×
1023
                      zip(f_mask, self.facets) if m]
1024
                list_kws = [k for k, vl in f_kwargs.items()
×
1025
                            if isinstance(vl, list)]
1026
                plt_kwargs = {k: vl for k, vl in f_kwargs.items() if
×
1027
                              k not in list_kws}
1028
                for k in list_kws:
×
1029
                    v = [val for val, m in zip(f_kwargs[k], f_mask) if m]
×
1030
                    plt_kwargs[k] = v
×
1031
            else:
1032
                xy = [np.array([self.points[kp] for kp in f]) for f in
×
1033
                      self.facets]
1034
                plt_kwargs = f_kwargs
×
1035
            pc = Poly3DCollection(xy, **plt_kwargs)
×
1036
            ax.add_collection(pc)
×
1037

1038
        # Adjust Axes
1039
        mins = np.array(self.points).min(axis=0)
×
1040
        maxs = np.array(self.points).max(axis=0)
×
1041

1042
        xlim = (min(xlim[0], mins[0]), max(xlim[1], maxs[0]))
×
1043
        ylim = (min(ylim[0], mins[1]), max(ylim[1], maxs[1]))
×
1044
        if n_dim == 2:
×
1045
            plt.axis('square')
×
1046
            plt.xlim(xlim)
×
1047
            plt.ylim(ylim)
×
1048
        if n_dim == 3:
×
1049
            zlim = (min(zlim[0], mins[2]), max(zlim[1], maxs[2]))
×
1050

1051
            ax.set_xlim(xlim)
×
1052
            ax.set_ylim(ylim)
×
1053
            ax.set_zlim(zlim)
×
1054
            _misc.axisEqual3D(ax)
×
1055

1056
    # ----------------------------------------------------------------------- #
1057
    # Mesh Equality                                                           #
1058
    # ----------------------------------------------------------------------- #
1059
    def __eq__(self, other_mesh):
12✔
1060
        # check type
1061
        if type(other_mesh) is not PolyMesh:
12✔
1062
            print('not same type')
12✔
1063
            return False
12✔
1064

1065
        # check that the lengths are all the same
1066
        same = True
12✔
1067
        same &= len(self.points) == len(other_mesh.points)
12✔
1068
        same &= len(self.facets) == len(other_mesh.facets)
12✔
1069
        same &= len(self.regions) == len(other_mesh.regions)
12✔
1070
        same &= len(self.seed_numbers) == len(other_mesh.seed_numbers)
12✔
1071
        same &= len(self.phase_numbers) == len(other_mesh.phase_numbers)
12✔
1072
        if not same:
12✔
1073
            print('not same length')
12✔
1074
            return False
12✔
1075

1076
        # check that the vertices have the same coordinates
1077
        pt_dists = distance.cdist(self.points, other_mesh.points)
12✔
1078
        same_pt = np.isclose(pt_dists, 0)
12✔
1079
        same_ints = same_pt.astype(int)
12✔
1080
        same &= np.all(same_ints.sum(axis=0) == 1)
12✔
1081
        same &= np.all(same_ints.sum(axis=1) == 1)
12✔
1082
        if not same:
12✔
1083
            print('not same verts')
12✔
1084
            return False
12✔
1085

1086
        kp_conv = np.argwhere(same_pt)
12✔
1087
        kp_other = kp_conv[:, 1]
12✔
1088
        print('transform')
12✔
1089
        print(np.array(kp_other))
12✔
1090

1091
        # check that the facets are the same
1092
        facets_in_other_kps = [[kp_other[kp] for kp in f] for f in self.facets]
12✔
1093
        o_fnum = []
12✔
1094
        for i, s_facet in enumerate(facets_in_other_kps):
12✔
1095
            for j, o_facet in enumerate(other_mesh.facets):
12✔
1096
                if j in o_fnum:
12✔
1097
                    continue
12✔
1098
                else:
1099
                    if set(s_facet) == set(o_facet):
12✔
1100
                        o_fnum.append(j)
12✔
1101
                        break
12✔
1102

1103
            if len(o_fnum) != i + 1:
12✔
1104
                print('not same facets')
12✔
1105
                return False
12✔
1106

1107
        # check that the regions are the same
1108
        regions_in_other_fnums = [[o_fnum[f] for f in r] for r in self.regions]
12✔
1109
        o_rnum = []
12✔
1110
        for i, s_region in enumerate(regions_in_other_fnums):
12✔
1111
            for j, o_region in enumerate(other_mesh.regions):
12✔
1112
                if j in o_rnum:
12✔
1113
                    continue
12✔
1114
                else:
1115
                    if set(s_region) == set(o_region):
12✔
1116
                        o_rnum.append(j)
12✔
1117
                        break
12✔
1118

1119
            if len(o_rnum) != i + 1:
12✔
1120
                print('not same regions')
12✔
1121
                return False
12✔
1122

1123
        # check that the seed numbers are the same
1124
        s_seed_nums = np.array(self.seed_numbers)
12✔
1125
        o_seed_nums = np.array(other_mesh.seed_numbers)
12✔
1126
        same &= np.all(s_seed_nums == o_seed_nums[o_rnum])
12✔
1127
        print('checking seed numbers', same)
12✔
1128

1129
        # check that the phase numbers are the same
1130
        s_phase_nums = np.array(self.phase_numbers)
12✔
1131
        o_phase_nums = np.array(other_mesh.phase_numbers)
12✔
1132
        same &= np.all(s_phase_nums == o_phase_nums[o_rnum])
12✔
1133
        print('checking phase numbers', same)
12✔
1134

1135
        return same
12✔
1136

1137

1138
def kp_loop(kp_pairs):
12✔
1139
    loop = list(kp_pairs[0])
12✔
1140
    kp_arr = np.array(kp_pairs[1:])
12✔
1141
    while kp_arr.shape[0] > 0:
12✔
1142
        kp_find = loop[-1]
12✔
1143
        has_kp = np.any(kp_arr == kp_find, axis=1)
12✔
1144
        row = kp_arr[has_kp]
12✔
1145

1146
        loop.append(row[row != kp_find][0])
12✔
1147
        kp_arr = kp_arr[~has_kp]
12✔
1148
    assert loop[0] == loop[-1]
12✔
1149
    return loop[:-1]
12✔
1150

1151

1152
def _clip_cell(cell_data, domain):
12✔
1153
    domain_name = type(domain).__name__.lower()
12✔
1154
    if domain_name in ['rectangle', 'square', 'box', 'cube']:
12✔
1155
        return cell_data
12✔
1156

1157
    if domain.n_dim == 2:
×
1158
        pts = np.array(cell_data['vertices'])
×
1159
        if np.all(domain.within(pts)):
×
1160
            return cell_data
×
1161

1162
        # split the edges that contain the boundary
1163
        new_adj = np.copy(cell_data['adjacency'])
×
1164
        new_faces = []
×
1165
        new_pts = np.copy(cell_data['vertices'])
×
1166
        new_kps = []
×
1167

1168
        for face in cell_data['faces']:
×
1169
            adj_cell = face['adjacent_cell']
×
1170
            verts = face['vertices']
×
1171
            face_pts = pts[verts]
×
1172
            pts_within = domain.within(face_pts)
×
1173
            if np.all(pts_within) or np.all(~pts_within):
×
1174
                new_faces.append(face)
×
1175
                continue
×
1176
            crossing_pt = _segment_cross(face_pts, domain)
×
1177

1178
            # Add point to list of vertices and face to list of faces
1179
            crossing_kp = len(new_pts)
×
1180
            new_pts = np.vstack((new_pts, crossing_pt.reshape(1, -1)))
×
1181
            new_kps.append(crossing_kp)
×
1182

1183
            for kp_i, kp in enumerate(verts):
×
1184
                kp_other = verts[1 - kp_i]
×
1185
                new_adj[kp] = [kp_other, crossing_kp]
×
1186

1187
                new_verts = [kp, crossing_kp]
×
1188
                new_faces.append({'adjacent_cell': adj_cell,
×
1189
                                  'vertices': new_verts})
1190

1191
        # add divider face
1192
        new_faces.append({'adjacent_cell': -1, 'vertices': new_kps})
×
1193

1194
        # Create cell within the domain
1195
        new_within = domain.within(new_pts)
×
1196
        new_within[new_kps] = True
×
1197

1198
        within_pts = new_pts[new_within]
×
1199
        kp_conv = np.full(len(new_pts), -1, dtype='int')
×
1200
        kp_conv[new_within] = np.arange(np.sum(new_within))
×
1201

1202
        within_adj = [[] for pt in within_pts]
×
1203
        within_faces = []
×
1204
        for face in new_faces:
×
1205
            adj_cell = face['adjacent_cell']
×
1206
            old_verts = face['vertices']
×
1207
            new_verts = [kp_conv[v] for v in old_verts]
×
1208
            within_face = {'adjacent_cell': adj_cell, 'vertices': new_verts}
×
1209
            if all([v >= 0 for v in new_verts]):
×
1210
                within_faces.append(within_face)
×
1211
                within_adj[new_verts[0]].append(new_verts[1])
×
1212
                within_adj[new_verts[1]].append(new_verts[0])
×
1213

1214
        # Compute cell area
1215
        within_loop = kp_loop([f['vertices'] for f in within_faces])
×
1216
        within_area = _loop_area(within_pts, within_loop)
×
1217

1218
        new_cell_data = {'adjacency': within_adj,
×
1219
                         'faces': within_faces,
1220
                         'original': cell_data['original'],
1221
                         'vertices': within_pts,
1222
                         'volume': within_area}
1223

1224
        return new_cell_data
×
1225

1226
    w_str = 'Cannot clip cells to fit to a ' + domain_name + '.'
×
1227
    w_str = ' Currently 3D geometries are not supported, other than boxes.'
×
1228
    warnings.warn(w_str, RuntimeWarning)
×
1229
    return cell_data
×
1230

1231

1232
def _segment_cross(pts, domain):
12✔
1233
    end_pts = np.copy(pts)
×
1234
    ds = np.inf
×
1235
    while ds > 1e-12:
×
1236
        within = domain.within(end_pts)
×
1237
        pt = end_pts.mean(axis=0)
×
1238
        pt_within = domain.within(pt)
×
1239
        if within[0] == pt_within:
×
1240
            end_pts[0] = pt
×
1241
        else:
1242
            end_pts[1] = pt
×
1243
        dx = end_pts[1] - end_pts[0]
×
1244
        ds = np.linalg.norm(dx)
×
1245
    return pt
×
1246

1247

1248
def _loop_area(pts, loop):
12✔
1249
    double_area = 0
×
1250

1251
    n = len(loop)
×
1252
    for i in range(n):
×
1253
        ip1 = (i + 1) % n
×
1254

1255
        xi = pts[i][0]
×
1256
        yi = pts[i][1]
×
1257
        xip1 = pts[ip1][0]
×
1258
        yip1 = pts[ip1][1]
×
1259

1260
        det = xi * yip1 - xip1 * yi
×
1261
        double_area += det
×
1262
    return 0.5 * np.abs(double_area)
×
1263

1264

1265
def _is_outward(pt_list, voropp_face_num):
12✔
1266
    n_dim = len(pt_list[0])
12✔
1267

1268
    voropp_sgn = 1-2*(voropp_face_num % 2)
12✔
1269
    voropp_axis = int((-voropp_face_num - 1)/2)
12✔
1270
    face_vec = voropp_sgn * np.eye(n_dim)[voropp_axis]
12✔
1271

1272
    if n_dim == 2:
12✔
1273
        pt1 = pt_list[0]
12✔
1274
        pt2 = pt_list[1]
12✔
1275
        rel_pos = np.array(pt2) - np.array(pt1)
12✔
1276
        n_vec = np.array([-rel_pos[1], rel_pos[0]])
12✔
1277
    elif n_dim == 3:
×
1278
        pt1 = pt_list[0]
×
1279
        pt2 = pt_list[1]
×
1280
        pt3 = pt_list[2]
×
1281
        r1 = np.array(pt2) - np.array(pt1)
×
1282
        r2 = np.array(pt3) - np.array(pt1)
×
1283
        n_vec = np.cross(r1, r2)
×
1284
    else:
1285
        raise ValueError('Function does not support {}D.'.format(n_dim))
×
1286

1287
    n_u = n_vec / np.linalg.norm(n_vec)
12✔
1288
    return np.dot(n_u, face_vec) > 0
12✔
1289

1290

1291
def _edge_lengths(pmesh):
12✔
1292
    edge_lens = {}  # (kp1, kp2): {'length': #, 'regions': set()}
×
1293
    for i, f in enumerate(pmesh.facets):
×
1294
        n = len(f)
×
1295
        facet_kp_pairs = [(f[k], f[(k + 1) % n]) for k in range(n)]
×
1296
        for pair in facet_kp_pairs:
×
1297
            key = tuple(sorted(pair))
×
1298
            if key not in edge_lens:  # calculate edge length
×
1299
                pt1 = pmesh.points[key[0]]
×
1300
                pt2 = pmesh.points[key[1]]
×
1301
                rel_pos = np.array(pt2) - np.array(pt1)
×
1302
                edge_len = np.linalg.norm(rel_pos)
×
1303
                edge_lens[key] = {
×
1304
                    'length': edge_len,
1305
                    'regions': set(),
1306
                    }
1307
            neighs = pmesh.facet_neighbors[i]
×
1308
            edge_lens[key]['regions'] |= set(neighs)
×
1309
    return edge_lens
×
1310

1311

1312
def _shortest_edge(edge_lens):
12✔
1313
    min_len = float('inf')
×
1314
    min_pair = (-1, -1)
×
1315
    for pair in edge_lens:
×
1316
        length = edge_lens[pair]['length']
×
1317
        if length < min_len:
×
1318
            min_len = length
×
1319
            min_pair = pair
×
1320
    return min_pair
×
1321

1322

1323
def _point_line_vec(pt, line_pts):
12✔
1324
    ptA, ptB = line_pts
×
1325
    n_vec = (ptB - ptA) / np.linalg.norm(ptB - ptA)
×
1326

1327
    rel_pos = ptA - pt
×
1328
    proj = np.dot(rel_pos, n_vec) * n_vec
×
1329
    dist_vec = rel_pos - proj
×
1330

1331
    u_vec = dist_vec / np.linalg.norm(dist_vec)
×
1332
    return u_vec
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc