Skip to content

Commit 75941f0

Browse files
committed
fix: ✨ bounding box object
a new class to store a bounding box, provides useful functions and accessors
1 parent b5be32e commit 75941f0

File tree

1 file changed

+134
-0
lines changed

1 file changed

+134
-0
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
from __future__ import annotations
2+
from typing import Optional
3+
from LoopStructural.utils.exceptions import LoopValueError
4+
import numpy as np
5+
6+
7+
class BoundingBox:
8+
def __init__(
9+
self,
10+
dimensions: int = 3,
11+
origin: Optional[np.ndarray] = None,
12+
maximum: Optional[np.ndarray] = None,
13+
nsteps: Optional[np.ndarray] = None,
14+
):
15+
self._origin = origin
16+
self._maximum = maximum
17+
self.dimensions = dimensions
18+
if nsteps is None:
19+
self.nsteps = np.array([50, 50, 25])
20+
self.name_map = {
21+
"xmin": (0, 0),
22+
"ymin": (0, 1),
23+
"zmin": (0, 2),
24+
"xmax": (1, 0),
25+
"ymax": (1, 1),
26+
"zmax": (1, 2),
27+
"lower": (0, 2),
28+
"upper": (1, 2),
29+
"minx": (0, 0),
30+
"miny": (0, 1),
31+
"minz": (0, 2),
32+
"maxx": (1, 0),
33+
"maxy": (1, 1),
34+
"maxz": (1, 2),
35+
}
36+
37+
@property
38+
def origin(self) -> np.ndarray:
39+
if self._origin is None:
40+
raise LoopValueError("Origin is not set")
41+
return self._origin
42+
43+
@origin.setter
44+
def origin(self, origin: np.ndarray):
45+
self._origin = origin
46+
47+
@property
48+
def maximum(self) -> np.ndarray:
49+
if self._maximum is None:
50+
raise LoopValueError("Maximum is not set")
51+
return self._maximum
52+
53+
@maximum.setter
54+
def maximum(self, maximum: np.ndarray):
55+
self._maximum = maximum
56+
57+
@property
58+
def nelements(self):
59+
return self.nsteps.prod()
60+
61+
@property
62+
def volume(self):
63+
return np.product(self.maximum - self.origin)
64+
65+
@nelements.setter
66+
def nelements(self, nelements):
67+
box_vol = self.volume
68+
ele_vol = box_vol / nelements
69+
# calculate the step vector of a regular cube
70+
step_vector = np.zeros(3)
71+
step_vector[:] = ele_vol ** (1.0 / 3.0)
72+
# step_vector /= np.array([1,1,2])
73+
# number of steps is the length of the box / step vector
74+
nsteps = np.ceil((self.maximum - self.origin) / step_vector).astype(int)
75+
self.nsteps = nsteps
76+
77+
def fit(self, locations: np.ndarray):
78+
if locations.shape[1] != self.dimensions:
79+
raise LoopValueError(
80+
f"locations array is {locations.shape[1]}D but bounding box is {self.dimensions}"
81+
)
82+
self.origin = locations.min(axis=0)
83+
self.maximum = locations.max(axis=0)
84+
return self
85+
86+
def with_buffer(self, buffer: float = 0.2) -> BoundingBox:
87+
if self.origin is None or self.maximum is None:
88+
raise LoopValueError(
89+
"Cannot create bounding box with buffer, no origin or maximum"
90+
)
91+
origin = self.origin - buffer * (self.maximum - self.origin)
92+
maximum = self.maximum + buffer * (self.maximum - self.origin)
93+
return BoundingBox(origin=origin, maximum=maximum)
94+
95+
def get_value(self, name):
96+
ix, iy = self.name_map.get(name, (-1, -1))
97+
if ix == -1 and iy == -1:
98+
raise LoopValueError(f"{name} is not a valid bounding box name")
99+
if iy == -1:
100+
return self.origin[ix]
101+
102+
return self.bb[ix,]
103+
104+
def __getitem__(self, name):
105+
if isinstance(name, str):
106+
return self.get_value(name)
107+
elif isinstance(name, tuple):
108+
return self.origin
109+
return self.get_value(name)
110+
111+
def is_inside(self, xyz):
112+
inside = np.zeros(xyz.shape[0], dtype=bool)
113+
inside = np.logical_and(inside, xyz[:, 0] > self.origin[0])
114+
inside = np.logical_and(inside, xyz[:, 0] < self.maximum[0])
115+
inside = np.logical_and(inside, xyz[:, 1] > self.origin[1])
116+
inside = np.logical_and(inside, xyz[:, 1] < self.maximum[1])
117+
inside = np.logical_and(inside, xyz[:, 2] > self.origin[2])
118+
inside = np.logical_and(inside, xyz[:, 2] < self.maximum[2])
119+
return inside
120+
121+
def regular_grid(self, nsteps=None, shuffle=False, order="C"):
122+
if nsteps is None:
123+
nsteps = self.nsteps
124+
x = np.linspace(self.origin[0], self.maximum[0], nsteps[0])
125+
y = np.linspace(self.origin[1], self.maximum[1], nsteps[1])
126+
z = np.linspace(self.origin[2], self.maximum[2], nsteps[2])
127+
xx, yy, zz = np.meshgrid(x, y, z, indexing="ij")
128+
locs = np.array(
129+
[xx.flatten(order=order), yy.flatten(order=order), zz.flatten(order=order)]
130+
).T
131+
if shuffle:
132+
# logger.info("Shuffling points")
133+
np.random.shuffle(locs)
134+
return locs

0 commit comments

Comments
 (0)