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

kip-hart / MicroStructPy / 6422648771

05 Oct 2023 05:53PM UTC coverage: 18.583%. Remained the same
6422648771

push

github-actions

web-flow
add support for Python 3.12 (#92)

3 of 3 new or added lines in 2 files covered. (100.0%)

978 of 5263 relevant lines covered (18.58%)

2.79 hits per line

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

21.5
/src/microstructpy/geometry/rectangle.py
1
# --------------------------------------------------------------------------- #
2
#                                                                             #
3
# Import Modules                                                              #
4
#                                                                             #
5
# --------------------------------------------------------------------------- #
6
from __future__ import division
15✔
7

8
import numpy as np
15✔
9
from matplotlib import patches
15✔
10
from matplotlib import pyplot as plt
15✔
11

12
from microstructpy.geometry.n_box import NBox
15✔
13

14
__author__ = 'Kenneth (Kip) Hart'
15✔
15

16

17
# --------------------------------------------------------------------------- #
18
#                                                                             #
19
# Rectangle Class                                                             #
20
#                                                                             #
21
# --------------------------------------------------------------------------- #
22
class Rectangle(NBox):
15✔
23
    """Rectangle
24

25
    This class contains a generic, 2D rectangle. The position and dimensions
26
    of the box can be specified using any of the parameters below.
27

28
    Without parameters, this returns a unit square centered on the origin.
29

30
    Args:
31
        length (float): *(optional)* Length of the rectangle.
32
        width (float): *(optional)* Width of the rectangle. *(optional)*
33
        side_lengths (list): *(optional)* Side lengths. Defaults to (1, 1).
34
        center (list): *(optional)* Center of rectangle. Defaults to (0, 0).
35
        corner (list): *(optional)* Bottom-left corner.
36
        bounds (list): *(optional)* Bounds of rectangle. Expected to be in the
37
            format [(xmin, xmax), (ymin, ymax)].
38
        limits : Alias for *bounds*.
39
        angle (float): *(optional)* The rotation angle, in degrees.
40
        angle_deg (float): *(optional)* The rotation angle, in degrees.
41
        angle_rad (float): *(optional)* The rotation angle, in radians.
42
        matrix (numpy.ndarray): *(optional)* The 2x2 rotation matrix.
43
    """
44

45
    def __init__(self, **kwargs):
15✔
46
        if 'length' in kwargs and 'width' in kwargs:
15✔
47
            kwargs['side_lengths'] = [kwargs['length'], kwargs['width']]
×
48

49
        if 'angle' in kwargs:
15✔
50
            cp = np.cos(np.radians(kwargs['angle']))
×
51
            sp = np.sin(np.radians(kwargs['angle']))
×
52
            kwargs['matrix'] = np.array([[cp, -sp], [sp, cp]])
×
53
        elif 'angle_deg' in kwargs:
15✔
54
            cp = np.cos(np.radians(kwargs['angle_deg']))
×
55
            sp = np.sin(np.radians(kwargs['angle_deg']))
×
56
            kwargs['matrix'] = np.array([[cp, -sp], [sp, cp]])
×
57
        elif 'angle_rad' in kwargs:
15✔
58
            cp = np.cos(kwargs['angle_rad'])
×
59
            sp = np.sin(kwargs['angle_rad'])
×
60
            kwargs['matrix'] = np.array([[cp, -sp], [sp, cp]])
×
61

62
        NBox.__init__(self, **kwargs)
15✔
63
        try:
15✔
64
            self.center
15✔
65
        except AttributeError:
×
66
            self.center = [0, 0]
×
67

68
        try:
15✔
69
            self.side_lengths
15✔
70
        except AttributeError:
×
71
            self.side_lengths = [1, 1]
×
72

73
    # ----------------------------------------------------------------------- #
74
    # Best Fit Function                                                       #
75
    # ----------------------------------------------------------------------- #
76
    def best_fit(self, points):
15✔
77
        """Find rectangle of best fit for points
78

79
        Args:
80
            points (list): List of points to fit.
81

82
        Returns:
83
            Rectangle: an instance of the class that best fits the points.
84
        """
85
        # Unpack the input points
86
        pts = np.array(points, dtype='float')
×
87
        x, y = pts.T
×
88

89
        # Find the most likely orientation for the rectangle
90
        A = np.vstack([x, np.ones(len(x))]).T
×
91
        m, _ = np.linalg.lstsq(A, y, rcond=None)[0]
×
92
        theta = np.arctan(m)
×
93

94
        # Rotate the points to an axis-aligned frame
95
        s = np.sin(theta)
×
96
        c = np.cos(theta)
×
97
        rot = np.array([[c, -s], [s, c]])
×
98
        pts_prin = pts.dot(rot)
×
99

100
        # Translate points to center of bounding box
101
        mins = pts_prin.min(axis=0)
×
102
        maxs = pts_prin.max(axis=0)
×
103
        bb_cen = 0.5 * (mins + maxs)
×
104
        pts_bb = pts_prin - bb_cen
×
105

106
        x_min, y_min = pts_bb.min(axis=0)
×
107
        x_max, y_max = pts_bb.max(axis=0)
×
108

109
        # Find nearest edge
110
        x1_dist = np.abs(pts_bb[:, 0] - x_min)
×
111
        x2_dist = np.abs(pts_bb[:, 0] - x_max)
×
112
        y1_dist = np.abs(pts_bb[:, 1] - y_min)
×
113
        y2_dist = np.abs(pts_bb[:, 1] - y_max)
×
114

115
        dists = np.array([x1_dist, x2_dist, y1_dist, y2_dist]).T
×
116
        min_dist = dists.min(axis=1)
×
117

118
        mask_x1 = np.isclose(x1_dist, min_dist)
×
119
        mask_x2 = np.isclose(x2_dist, min_dist)
×
120
        mask_y1 = np.isclose(y1_dist, min_dist)
×
121
        mask_y2 = np.isclose(y2_dist, min_dist)
×
122

123
        # Group points by edge
124
        x_x1 = pts_bb[mask_x1, 0].T
×
125
        y_y1 = pts_bb[mask_y1, 1].T
×
126
        x_x2 = pts_bb[mask_x2, 0].T
×
127
        y_y2 = pts_bb[mask_y2, 1].T
×
128

129
        # Get lines of best fit for each edge
130
        x1_fit = np.mean(x_x1)
×
131
        y1_fit = np.mean(y_y1)
×
132
        x2_fit = np.mean(x_x2)
×
133
        y2_fit = np.mean(y_y2)
×
134

135
        x_cen = 0.5 * (x1_fit + x2_fit)
×
136
        y_cen = 0.5 * (y1_fit + y2_fit)
×
137
        fit_cen_bb = np.array([x_cen, y_cen])
×
138
        x_len = x2_fit - x1_fit
×
139
        y_len = y2_fit - y1_fit
×
140

141
        # Translate center to rotated frame
142
        fit_cen_prin = fit_cen_bb + bb_cen
×
143
        fit_cen = rot.dot(fit_cen_prin.reshape(-1, 1)).flatten()
×
144

145
        # Disambiguate the orientation and axes
146
        x_ax_seed = np.array(self.matrix)[:, 0]
×
147
        x_dot, y_dot = rot.T.dot(x_ax_seed)
×
148

149
        if np.abs(x_dot) > np.abs(y_dot):
×
150
            if x_dot > 0:
×
151
                x_ax_fit = rot[:, 0]
×
152
            else:
153
                x_ax_fit = - rot[:, 0]
×
154

155
            length = x_len
×
156
            width = y_len
×
157
        else:
158
            if y_dot > 0:
×
159
                x_ax_fit = rot[:, 1]
×
160
            else:
161
                x_ax_fit = - rot[:, 1]
×
162

163
            length = y_len
×
164
            width = x_len
×
165

166
        ang_diff = np.arcsin(np.cross(x_ax_seed, x_ax_fit))
×
167
        ang_rad = self.angle_rad + ang_diff
×
168

169
        rect_fit = Rectangle(center=fit_cen, length=length, width=width,
×
170
                             angle_rad=ang_rad)
171

172
        return rect_fit
×
173

174
    # ----------------------------------------------------------------------- #
175
    # Representation Function                                                 #
176
    # ----------------------------------------------------------------------- #
177
    def __repr__(self):
15✔
178
        repr_str = 'Rectangle('
×
179
        repr_str += 'center=' + repr(tuple(self.center)) + ', '
×
180
        repr_str += 'side_lengths=' + repr(tuple(self.side_lengths)) + ', '
×
181
        repr_str += 'angle=' + repr(self.angle) + ')'
×
182
        return repr_str
×
183

184
    # ----------------------------------------------------------------------- #
185
    # Number of Dimensions                                                    #
186
    # ----------------------------------------------------------------------- #
187
    @property
15✔
188
    def n_dim(self):
15✔
189
        """int: Number of dimensions, 2"""
190
        return 2
15✔
191

192
    # ----------------------------------------------------------------------- #
193
    # Area                                                                    #
194
    # ----------------------------------------------------------------------- #
195
    @property
15✔
196
    def area(self):
15✔
197
        """float: Area of rectangle"""
198
        return self.n_vol
×
199

200
    @classmethod
15✔
201
    def area_expectation(cls, **kwargs):
15✔
202
        r"""Expected area of rectangle
203

204
        This method computes the expected area of a rectangle. There are two
205
        main ways to define the size of a rectangle: by the length and width
206
        and by the bounds. If the input rectangle is defined by length and
207
        width, the expected area is:
208

209
        .. math::
210

211
            \mathbb{E}[A] = \mathbb{E}[L W] = \mu_L \mu_W
212

213
        For the case where it is defined by upper and lower bounds:
214

215
        .. math::
216

217
            \mathbb{E}[A] = \mathbb{E}[(X_U - X_L) (Y_U - Y_L)]
218

219
        .. math::
220
            \mathbb{E}[A] =
221
            \mu_{X_U}\mu_{Y_U} - \mu_{X_U} \mu_{Y_L} -
222
            \mu_{X_L}\mu_{Y_U} + \mu_{X_L}\mu_{Y_L}
223

224
        Example:
225
            >>> import scipy.stats
226
            >>> import microstructpy as msp
227
            >>> L = scipy.stats.uniform(loc=1, scale=2)
228
            >>> W = scipy.stats.norm(loc=3.2, scale=5.1)
229
            >>> L.mean() * W.mean()
230
            6.4
231
            >>> msp.geometry.Rectangle.area_expectation(length=L, width=W)
232
            6.4
233

234
        Args:
235
            **kwargs: Keyword arguments, same as :class:`.Rectangle` but the
236
                inputs can be from the :mod:`scipy.stats` module.
237

238
        Returns:
239
            float: Expected/average area of rectangle.
240

241
        """
242
        if 'length' in kwargs or 'width' in kwargs:
×
243
            len_dist = kwargs.get('length', 1)
×
244
            width_dist = kwargs.get('width', 1)
×
245
            return _prod_exp(*[len_dist, width_dist])
×
246

247
        if 'side_lengths' in kwargs:
×
248
            return _prod_exp(*kwargs['side_lengths'])
×
249

250
        if 'bounds' in kwargs:
×
251
            x_bnds, y_bnds = kwargs['bounds']
×
252
            x_lb, x_ub = x_bnds
×
253
            y_lb, y_ub = y_bnds
×
254

255
            p1 = _prod_exp(*[x_ub, y_ub])
×
256
            p2 = _prod_exp(*[x_ub, y_lb])
×
257
            p3 = _prod_exp(*[x_lb, y_ub])
×
258
            p4 = _prod_exp(*[x_lb, y_lb])
×
259

260
            return p1 - p2 - p3 + p4
×
261

262
        raise ValueError('Cannot find the expected area of rectangle')
×
263

264
    # ----------------------------------------------------------------------- #
265
    # Properties                                                              #
266
    # ----------------------------------------------------------------------- #
267
    @property
15✔
268
    def length(self):
15✔
269
        """float: Length of rectangle, side length along 1st axis"""
270
        return self.side_lengths[0]
×
271

272
    @property
15✔
273
    def width(self):
15✔
274
        """float: Width of rectangle, side length along 2nd axis"""
275
        return self.side_lengths[1]
×
276

277
    @property
15✔
278
    def angle(self):
15✔
279
        """float: Rotation angle of rectangle - degrees"""
280
        return self.angle_deg
×
281

282
    @property
15✔
283
    def angle_deg(self):
15✔
284
        """float: Rotation angle of rectangle - degrees"""
285
        return 180 * self.angle_rad / np.pi
×
286

287
    @property
15✔
288
    def angle_rad(self):
15✔
289
        """float: Rotation angle of rectangle - radians"""
290
        return np.arctan2(self.matrix[1][0], self.matrix[0][0])
×
291

292
    # ----------------------------------------------------------------------- #
293
    # Circle Approximation                                                    #
294
    # ----------------------------------------------------------------------- #
295
    def approximate(self, x1=None):
15✔
296
        """Approximate rectangle with a set of circles.
297

298
        This method approximates a rectangle with a set of circles.
299
        These circles are spaced uniformly along the long axis of the
300
        rectangle with distance ``x1`` between them.
301

302
        Example
303
        -------
304

305
        For a rectangle with length=2.5, width=1, and x1=0.3,
306
        the approximation would look like :numref:`f_api_rectangle_approx`.
307

308
        .. _f_api_rectangle_approx:
309
        .. figure:: ../../auto_examples/geometry/images/sphx_glr_plot_rectangle_001.png
310

311
            Circular approximation of rectangle.
312

313
        Args:
314
            x1 (float or None): *(optional)* Spacing between the circles.
315
                If not specified, the spacing is 0.25x the length of the
316
                shortest side.
317

318
        Returns:
319
            numpy.ndarray: An Nx3 array, where each row is a circle and the
320
            columns are x, y, and r.
321

322
        """  # NOQA: E501
323
        if x1 is None:
×
324
            x1 = 0.25 * min(self.side_lengths)
×
325

326
        if self.side_lengths[0] >= self.side_lengths[1]:
×
327
            length = self.side_lengths[0]
×
328
            width = self.side_lengths[1]
×
329
            inds = [0, 1]
×
330
        else:
331
            length = self.side_lengths[1]
×
332
            width = self.side_lengths[0]
×
333
            inds = [1, 0]
×
334

335
        # Centerline circles
336
        xc = 0
×
337
        half_len = 0.5 * length
×
338
        r = 0.5 * width
×
339
        circs = []
×
340
        while xc < half_len:
×
341
            circ = [xc, 0, r]
×
342
            circs.append(circ)
×
343

344
            if np.isclose(xc + r, half_len):
×
345
                xc = half_len
×
346
            elif xc + x1 > half_len - r:
×
347
                xc = half_len - r
×
348
            else:
349
                xc = xc + x1
×
350

351
        # Corner circle
352
        while r > 0:
×
353
            x_init = circs[-1][0]
×
354
            y_init = circs[-1][1]
×
355
            x = x_init + x1
×
356
            y = y_init + x1
×
357

358
            dx = half_len - x
×
359
            dy = 0.5 * width - y
×
360
            r = min(dx, dy)
×
361
            if r <= 0:
×
362
                break
×
363
            else:
364
                circs.append([x, y, r])
×
365

366
        # Reflect circles
367
        circs = np.array(circs)
×
368
        for dim in range(self.n_dim):
×
369
            mask = circs[:, dim] > 0
×
370
            new_circs = np.copy(circs[mask])
×
371
            new_circs[:, dim] *= -1
×
372
            circs = np.concatenate((circs, new_circs))
×
373

374
        circs[:, inds] = circs[:, [0, 1]]
×
375

376
        # Rotate and translate circles
377
        pts = circs[:, :-1]
×
378
        circs[:, :-1] = pts.dot(np.array(self.matrix).T)
×
379
        circs[:, :-1] += self.center
×
380
        return circs
×
381

382
    # ----------------------------------------------------------------------- #
383
    # Plot Function                                                           #
384
    # ----------------------------------------------------------------------- #
385
    def plot(self, **kwargs):
15✔
386
        """Plot the rectangle.
387

388
        This function adds a :class:`matplotlib.patches.Rectangle` patch to the
389
        current axes. The keyword arguments are passed through to the patch.
390

391
        Args:
392
            **kwargs (dict): Keyword arguments for the patch.
393

394
        """  # NOQA: E501
395
        pt = self.corner
×
396
        w = self.length
×
397
        h = self.width
×
398
        ang = self.angle
×
399
        c = patches.Rectangle(pt, w, h, angle=ang, **kwargs)
×
400
        plt.gca().add_patch(c)
×
401

402

403
def _prod_exp(*args):
15✔
404
    prod = 1
×
405
    for arg in args:
×
406
        try:
×
407
            arg_mu = arg.moment(1)
×
408
        except AttributeError:
×
409
            arg_mu = arg
×
410
        prod *= arg_mu
×
411
    return prod
×
412

413

414
# --------------------------------------------------------------------------- #
415
#                                                                             #
416
# Square Class                                                                #
417
#                                                                             #
418
# --------------------------------------------------------------------------- #
419
class Square(Rectangle):
15✔
420
    """A square.
421

422
    This class contains a generic, 2D square. It is derived from the
423
    :class:`microstructpy.geometry.Rectangle` class and contains the
424
    ``side_length`` property, rather than multiple side lengths.
425

426
    Args:
427
        side_length (float): *(optional)* Side length. Defaults to 1.
428
        center (list): *(optional)* Center of rectangle. Defaults to (0, 0).
429
        corner (list): *(optional)* Bottom-left corner.
430
    """
431

432
    def __init__(self, **kwargs):
15✔
433
        if 'side_length' in kwargs:
×
434
            kwargs['side_lengths'] = 2 * [kwargs['side_length']]
×
435

436
        Rectangle.__init__(self, **kwargs)
×
437

438
    # ----------------------------------------------------------------------- #
439
    # Side Length Property                                                    #
440
    # ----------------------------------------------------------------------- #
441
    @property
15✔
442
    def side_length(self):
15✔
443
        """float: length of the side of the square."""
444
        return self.side_lengths[0]
×
445

446
    # ----------------------------------------------------------------------- #
447
    # Area                                                                    #
448
    # ----------------------------------------------------------------------- #
449
    @classmethod
15✔
450
    def area_expectation(cls, **kwargs):
15✔
451
        r"""Expected area of square
452

453
        This method computes the expected area of a square with distributed
454
        side length.
455
        The expectation is:
456

457
        .. math::
458

459
            \mathbb{E}[A] = \mathbb{E}[S^2] = \mu_S^2 + \sigma_S^2
460

461
        Example:
462
            >>> import scipy.stats
463
            >>> import microstructpy as msp
464
            >>> S = scipy.stats.expon(scale=2)
465
            >>> S.mean()^2 + S.var()
466
            8.0
467
            >>> msp.geometry.Square.area_expectation(side_length=S)
468
            8.0
469

470
        Args:
471
            **kwargs: Keyword arguments, same as :class:`.Square` but the
472
                inputs can be from the :mod:`scipy.stats` module.
473

474
        Returns:
475
            float: Expected/average area of the square.
476

477
        """
478
        if 'side_length' in kwargs:
×
479
            len_dist = kwargs['side_length']
×
480

481
            try:
×
482
                area_exp = len_dist.moment(2)
×
483
            except AttributeError:
×
484
                area_exp = len_dist * len_dist
×
485
            return area_exp
×
486

487
        Rectangle.area_expectation(**kwargs)
×
488

489
    # ----------------------------------------------------------------------- #
490
    # Circle Approximation                                                    #
491
    # ----------------------------------------------------------------------- #
492
    def approximate(self, x1=None):
15✔
493
        """Approximate square with a set of circles
494

495
        This method approximates a square with a set of circles.
496
        These circles are spaced uniformly along the edges of the square
497
        with distance ``x1`` between them.
498

499
        Example
500
        -------
501

502
        For a square with side_length=1, and x1=0.2,
503
        the approximation would look like :numref:`f_api_square_approx`.
504

505
        .. _f_api_square_approx:
506
        .. figure:: ../../auto_examples/geometry/images/sphx_glr_plot_rectangle_002.png
507

508
            Circular approximation of square.
509

510
        Args:
511
            x1 (float or None): *(optional)* Spacing between the circles.
512
                If not specified, the spacing is 0.25x the side length.
513

514
        Returns:
515
            numpy.ndarray: An Nx3 array, where each row is a circle and the
516
            columns are x, y, and r.
517

518
        """  # NOQA: E501
519
        return Rectangle.approximate(self, x1)
×
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